Orchestrata Ultimate / Specs / 105-ontology-domain-model
105-ontology-domain-model: Ontology-Driven Domain Model Generation
A skill that reads an ontology (OWL/RDFS in Turtle/.ttl, i.e. RDF triples) and generates a domain model as a screaming-architecture / modular monolith with centralized logging and exception handling. Generation is deterministic (Jinja2 templates → idempotent output) and bidirectional: a canonical intermediate representation (IR) drives both the ontology and the code, so adding a node/class regenerates both. Generated entities and relationships persist to Stardog...
At a glance
| Item | Value |
|---|---|
| Spec directory | specs/105-ontology-domain-model/ |
| Status line in the spec | In Review |
| Requirement IDs defined | 50 |
With a @traces citation in src/ |
28 (56.0%) |
With a @traces citation in tests/ |
0 (0.0%) |
| Tracked files | 2 |
| RTM page | 105-ontology-domain-model |
Reading this spec
This spec defines a skill that reads an ontology (OWL or RDFS in Turtle, that is RDF triples) and generates a domain model from it, as a screaming-architecture modular monolith with centralized logging and exception handling. Hand-writing a domain model and its persistence drifts from the ontology and buries audit history, so everything is driven from one canonical intermediate representation (IR). Generation is deterministic (Jinja2 templates, idempotent output) and bidirectional: the IR drives both the ontology and the code, so adding a node or class regenerates both. Generated entities and relationships persist to Stardog with temporal (bitemporal) and insert-only semantics for nodes and edges alike.
Four user stories cover generating a domain model from an ontology, adding a class and regenerating both ways, persisting with full history and no destructive writes, and getting centralized logging and exceptions in generated code. The eight functional requirements (FR-001 to FR-008) are ontology to IR, IR to ontology, round-trip, IR to code, centralized logging and exceptions, Stardog persistence, add-a-node bidirectional regeneration and constraint generation with SHACL.
The 50 requirement IDs (REQ-105-NNN) record the rules. An owl:Class or rdfs:Class becomes an Entity in the IR; rdfs:subClassOf sets a single parent (several parents keep the first with a warning); IRI local names are extracted while the full IRI is preserved; an object property without a range is skipped with a warning instead of crashing. Round-trip preserves namespace, labels, comments and cardinality. Every generated module uses core.logging.get_logger. Every assertion carries a transaction time, a logical delete inserts a tombstone that closes :validTo and never removes a triple, and generated SPARQL is parameterized with IRIs validated before interpolation. SHACL constraints are emitted as constraints.ttl, loadable into Stardog's integrity constraint validation.
The code is in the domaingen package: reader (15 requirements), generator, ttl, constraints and stardog. The requirements traceability matrix lists each ID. The spec's own status line reads In Review and it tracks 2 files. Measured coverage is 28 of 50 requirements cited by at least one @traces in src/ (56.0%), spread over 6 modules; the module citing the most of them is orchestrata_ultimate/domaingen/reader (15 requirements). In tests/, 0 requirements are cited by a test-side @traces. 22 requirements have no citing symbol, most of them in the bitemporal persistence group (REQ-105-027 to 039); the behaviour may still exist in code that was never annotated.
Requirements
Checkbox is the state recorded in the requirements checklist; Traced by is the count of src/ symbols whose @traces names the ID.
| ID | Done | Requirement | Traced by |
|---|---|---|---|
REQ-105-001 |
yes | owl:Class / rdfs:Class → an Entity in the IR. |
2 |
REQ-105-012 |
yes | Accept the ontology as a .ttl file path or raw Turtle text. |
2 |
REQ-105-013 |
yes | Parse via rdflib (Graph().parse, Turtle); RDF-star input tolerated. |
2 |
REQ-105-014 |
yes | owl:DatatypeProperty with rdfs:domain D → a Field on entity D. |
1 |
REQ-105-015 |
yes | owl:ObjectProperty with rdfs:domain D, rdfs:range R → a Relationship on D targeting R. |
2 |
REQ-105-016 |
yes | rdfs:subClassOf → Entity.parent (single; multiple parents → first + warning). |
2 |
REQ-105-002 |
yes | Cardinality — owl:FunctionalProperty / owl:maxCardinality 1 → many=False; otherwise many=True. |
none |
REQ-105-017 |
yes | owl:minCardinality >= 1 (or owl:someValuesFrom) → Field/Relationship.required=True. |
3 |
REQ-105-018 |
yes | xsd datatype → Python type map (string→str, integer/int→int, decimal/double/float→float, boolean→bool, dateTime/date→datetime); unknown → str + warning (ERR-105-003). |
1 |
REQ-105-019 |
yes | rdfs:label → human name (fallback: IRI local name); rdfs:comment → docstring. |
1 |
REQ-105-020 |
yes | IRI local-name extraction (after # or final /); preserve full iri on the IR node. |
1 |
REQ-105-021 |
yes | owl:inverseOf → Relationship.inverse. |
1 |
REQ-105-022 |
yes | Entities group into Modules (bounded contexts); default = one module named after the ontology. |
none |
REQ-105-023 |
yes | The ontology base IRI / prefix is captured on DomainModel.namespace. |
2 |
REQ-105-024 |
yes | Parse failure raises OntologyParseError(BaseError) with file + cause (ERR-105-001). |
2 |
REQ-105-025 |
yes | Object property with no rdfs:range is skipped with a warning (ERR-105-002), not a crash. |
2 |
REQ-105-003 |
yes | Render a DomainModel to valid Turtle (classes, datatype/object properties with rdfs:domain/rdfs:range, rdfs:subClassOf). |
5 |
REQ-105-004 |
yes | parse_ontology(render_ontology(ir)) equals ir structurally (entities, fields, relationships, inheritance). |
2 |
REQ-105-026 |
yes | TTL rendering is deterministic — sorted entities/properties, stable prefixes, no timestamps. | 4 |
REQ-105-011 |
yes | Adding an Entity and/or Relationship to the IR regenerates both the ontology TTL and the code consistently. |
1 |
REQ-105-027 |
yes | Round-trip preserves namespace, labels, comments, and cardinality. | none |
REQ-105-008 |
yes | The generated repository emits only INSERT SPARQL; no UPDATE/DELETE is ever produced. | 1 |
REQ-105-028 |
yes | Each node attribute is an RDF-star assertion << :e :attr v >> annotated with provenance/temporal metadata. |
none |
REQ-105-029 |
yes | Relationship (edge) triples << :a :rel :b >> are annotated identically — temporality applies to edges as well as nodes. |
none |
REQ-105-030 |
yes | Annotations are append-only; no existing triple/annotation is mutated in place. | none |
REQ-105-031 |
yes | Every assertion carries prov:txFrom (transaction time, when recorded, UTC xsd:dateTime). |
none |
REQ-105-032 |
yes | Every assertion carries :validFrom and :validTo (valid time, when true in the domain). |
none |
REQ-105-033 |
yes | Open intervals use a sentinel :validTo "9999-12-31T00:00:00Z" (or unbound), never NULL deletes. |
none |
REQ-105-009 |
yes | Changing a value/edge inserts a new annotated assertion and a tombstone that closes the prior :validTo — both are inserts. |
none |
REQ-105-034 |
yes | A tombstone is itself an inserted assertion; the original assertion remains untouched (full history retained). | none |
REQ-105-035 |
yes | "Logical delete" = insert a tombstone closing :validTo; the triple is never physically removed. |
none |
REQ-105-010 |
yes | as_of(T) returns node state at instant T via SPARQL-star: txFrom <= T ∧ validFrom <= T < validTo. |
none |
REQ-105-036 |
yes | as_of(T) works for relationships (edge history), not just node attributes. |
none |
REQ-105-037 |
yes | "Current state" view = latest txFrom, not tombstoned, validTo in the future. |
none |
REQ-105-038 |
yes | A separate transaction-time-only ("as-recorded") and valid-time-only query mode is supported. | none |
REQ-105-039 |
yes | Generated SPARQL is parameterized/escaped; ontology IRIs are validated before interpolation (SEC-105-002). | none |
REQ-105-040 |
yes | No DELETE/DROP/CLEAR is ever generated; a generation-time check fails the build if present (SEC-105-003). |
2 |
REQ-105-041 |
yes | A named-graph-per-version persistence mode is documented as an alternative to RDF-star (for stores without RDF-star). | none |
REQ-105-042 |
yes | Stardog connection failure raises RepositoryError/StardogConnectionError (both BaseError); code generation itself is offline and unaffected (ERR-105-005). |
5 |
REQ-105-049 |
yes | generate_shacl emits a sh:NodeShape per entity (sh:targetClass); fields → property shapes with sh:datatype + sh:maxCount 1 (+ sh:minCount 1 when required); relationships → sh:class <target> (+ sh:maxCount 1 single, sh:minCount 1 required); a subclass references its parent via sh:node. |
2 |
REQ-105-050 |
yes | SHACL is deterministic, valid Turtle, emitted as constraints.ttl by generate_code; loadable into Stardog ICV (on-demand icv report, or guard mode to enforce on write). |
1 |
REQ-105-005 |
yes | Per module, generate domain/<module>/{entities.py, repository.py, __init__.py} (relationships are entity attributes + the repository PREDICATES map). |
1 |
REQ-105-043 |
yes | Layout is screaming — top-level packages are named for the domain/bounded context, not the framework. | 1 |
REQ-105-044 |
yes | Modular monolith — a module imports only its own package + shared core; cross-module references go through relationship objects (no direct cross-module imports). |
none |
REQ-105-045 |
yes | Entity class is a dataclass with typed fields; parent reflected via inheritance; required fields have no default, optional default None. |
none |
REQ-105-007 |
yes | Every generated module uses core.logging.get_logger(__name__). |
1 |
REQ-105-046 |
yes | Domain errors inherit core.base.error.BaseError; class names end in Error. |
none |
REQ-105-047 |
yes | Generated code has no print and no bare except:. |
none |
REQ-105-006 |
yes | Codegen is deterministic/idempotent — re-running on the same IR yields byte-identical files (stable ordering, no timestamps). | 4 |
REQ-105-048 |
yes | All TTL/code writes are confined to the target root; path traversal is rejected (SEC-105-001). | 2 |
Code that traces this spec
| Module | Symbols | Requirements |
|---|---|---|
orchestrata_ultimate/core/logging |
1 | 1 |
orchestrata_ultimate/domaingen/constraints |
2 | 2 |
orchestrata_ultimate/domaingen/generator |
6 | 6 |
orchestrata_ultimate/domaingen/reader |
10 | 15 |
orchestrata_ultimate/domaingen/stardog |
5 | 1 |
orchestrata_ultimate/domaingen/ttl |
5 | 4 |
Checklists
checklists/requirements.md
The requirement list above comes from this file; 50 of 50 checkbox items are ticked.
Full spec text
Verbatim from specs/105-ontology-domain-model/spec.md; headings are pushed down two levels.
SPEC-105: Ontology-Driven Domain Model Generation
Metadata
- Spec ID: 105-ontology-domain-model
- Title: Ontology-Driven Domain Model Generation — Bidirectional Codegen with Temporal, Insert-Only Stardog Persistence
- Author: Mohamed
- Status: In Review
- Created: 2026-05-28
- Last Updated: 2026-05-28
- Priority: P2
- Version: 0.2.0
- Extends: SPEC-100 (Logging & Exceptions — generated code uses it)
Overview
What
A skill that reads an ontology (OWL/RDFS in Turtle/.ttl, i.e. RDF triples)
and generates a domain model as a screaming-architecture / modular
monolith with centralized logging and exception handling. Generation is
deterministic (Jinja2 templates → idempotent output) and bidirectional:
a canonical intermediate representation (IR) drives both the ontology and the
code, so adding a node/class regenerates both. Generated entities and
relationships persist to Stardog with temporal (bitemporal) and
insert-only semantics for both nodes and edges.
Why
Ontologies capture a domain's classes, attributes, and relationships precisely. Hand-writing the matching domain model + persistence drifts from the ontology and buries audit history. Driving everything from one canonical IR keeps the ontology, the code, and the graph in lock-step; insert-only + bitemporal storage gives a complete, queryable history ("as-of") with no destructive updates.
How (pipeline)
.ttl ontology ──(rdflib)──► DomainModel IR ──(Jinja2, deterministic)──► code
▲ (canonical) │
└──────────(render TTL)────────┴───────────(model registry)────────────┘
│
▼
Stardog (RDF-star, insert-only, bitemporal)
The IR is the single source of truth. ontology→IR and IR→ontology
round-trip; IR→code is deterministic. Adding an entity to the IR regenerates
the ontology and the code together (FR-007).
Architecture of the generated model
- Screaming architecture: the top-level package layout names the domain, not
the framework —
domain/<module>/per bounded context, each containingentities.py,repository.py,__init__.py. The structure "screams" the domain. - Modular monolith: one deployable; modules have explicit boundaries (a
module only imports its own + shared
core). Cross-module references go through relationship objects, not direct imports. - Centralized logging/exceptions: every generated module uses
core.logging.get_loggerand raises domain errors inheriting the SPEC-100BaseErrorhierarchy; noprint, no bareexcept.
Temporal, insert-only RDF model (Stardog)
Nodes (entity attribute assertions) and edges (relationship triples) are never updated or deleted. Each is recorded as an RDF-star–annotated assertion carrying bitemporal stamps:
<< :order_42 :total 19.99 >> prov:txFrom "2026-05-28T10:00:00Z" ;
:validFrom "2026-05-28T00:00:00Z" ;
:validTo "9999-12-31T00:00:00Z" .
<< :order_42 :placedBy :cust_7 >> prov:txFrom "2026-05-28T10:00:00Z" ; ... .
- Change = insert a new annotated assertion (new
txFrom); the prior value is logically superseded by a tombstone assertion (also an insert) that closes itsvalidTo— nothing is mutated in place. - Current state = assertions with the latest
txFromnot closed by a tombstone andvalidToin the future. - As-of (T) = SPARQL-star filtering
txFrom <= T(transaction time) andvalidFrom <= T < validTo(valid time). Same model for edges.
User Scenarios
US-001: Generate a domain model from an ontology
As a developer I want to point the skill at a .ttl ontology and get a
screaming-architecture domain model so that code matches the ontology exactly.
Acceptance Criteria: OWL classes → entity classes; datatype properties →
typed fields; object properties → relationships; rdfs:subClassOf → inheritance;
output is deterministic (re-running yields identical files).
US-002: Add a class/node and regenerate both ways
As a developer I want adding an entity to the IR to update both the
ontology and the code so that they never drift.
Acceptance Criteria: adding an entity + a relationship to the IR regenerates
the .ttl (new class + property) and the code (new module/entity/repository);
ontology→IR→ontology round-trips structurally.
US-003: Persist with full history, no destructive writes
As an operator I want all node and edge changes stored insert-only with time stamps so that I can query any past state and nothing is ever lost. Acceptance Criteria: the generated repository emits only INSERTs (RDF-star annotated); a value change inserts a new assertion + a tombstone; an "as-of" query returns the state at a past instant; no UPDATE/DELETE SPARQL is generated.
US-004: Centralized logging & exceptions in generated code
As a maintainer I want generated modules to use the shared logging and
exception framework so that observability and error handling are uniform.
Acceptance Criteria: every generated module imports core.logging.get_logger;
domain errors inherit the SPEC-100 base; generated code compiles and lints clean.
Data Models (IR)
| Type | Fields |
|---|---|
DomainModel |
name: str, namespace: str (ontology base IRI), modules: list[Module] |
Module |
name: str (bounded context), entities: list[Entity] |
Entity |
name: str, iri: str, parent: str | None, fields: list[Field], relationships: list[Relationship], doc: str |
Field |
name: str, type: str (python type from xsd), iri: str, required: bool, doc: str |
Relationship |
name: str, iri: str, target: str (target entity), many: bool, inverse: str | None, doc: str |
xsd→python map: xsd:string→str, xsd:int/integer→int, xsd:decimal/double/float→float,
xsd:boolean→bool, xsd:dateTime/date→datetime, default→str.
Functional Requirements
FR-001 — Ontology → IR
REQ-105-001: Parse a.ttl/OWL ontology (rdflib) into aDomainModel:owl:Class/rdfs:Class→ Entity;owl:DatatypeProperty(byrdfs:domain) → Field (type via xsd map);owl:ObjectProperty→ Relationship (rdfs:domain→owner,rdfs:range→target);rdfs:subClassOf→parent.REQ-105-002: Cardinality —owl:FunctionalProperty/owl:maxCardinality 1→ single (many=False); elsemany=True.owl:minCardinality >=1→required=True.
FR-002 — IR → Ontology
REQ-105-003: Render aDomainModelto a valid Turtle ontology (classes, datatype/object properties with domain/range, subClassOf), via a deterministic Jinja2 template.
FR-003 — Round-trip
REQ-105-004:parse(render(ir))equalsirstructurally (entities, fields, relationships, inheritance).
FR-004 — IR → Code (deterministic, screaming architecture)
REQ-105-005: Generate, per module, a packagedomain/<module>/withentities.py(one class per Entity, typed fields, inheritance),repository.py, and__init__.py— using Jinja2 templates. (Relationships are realized as attributes on the entity dataclasses plus the repositoryPREDICATESmap, not a separaterelationships.py.)REQ-105-006: Generation is deterministic/idempotent — stable ordering (sorted), no timestamps in output; re-running on the same IR yields byte-identical files.
FR-005 — Centralized logging & exceptions
REQ-105-007: Generated modules usecore.logging.get_logger(__name__); domain errors inherit the SPEC-100BaseErrorhierarchy; noprint/bareexcept.
FR-006 — Stardog persistence: temporal, insert-only (nodes + edges)
REQ-105-008: The generated repository writes only INSERTs — node attribute assertions and edge triples are RDF-star–annotated withtxFrom,validFrom,validTo; no UPDATE/DELETE SPARQL is produced.REQ-105-009: A value/edge change inserts a new annotated assertion and a tombstone closing the priorvalidTo; history is never destroyed.REQ-105-010: An as-of(T) query returns node and edge state at instant T (SPARQL-star ontxFrom/validFrom/validTo).
FR-007 — Add-a-node bidirectional regeneration
REQ-105-011: Adding an Entity (and/or Relationship) to the IR and regenerating updates both the ontology TTL and the code, consistently.
FR-008 — Constraint generation (Stardog ICV / SHACL)
Stardog Integrity Constraint Validation is expressed in SHACL; constraints
load into a database and run on demand (icv report) or enforce on write (guard mode).
REQ-105-049: Generate SHACL from the IR — ash:NodeShapeper entity (sh:targetClass); a property shape per field (sh:datatype+sh:maxCount 1, plussh:minCount 1when required) and per relationship (sh:class, sh:maxCount 1when single,sh:minCount 1when required); a subclass references its parent shape viash:node.REQ-105-050: SHACL output is deterministic, valid Turtle, and emitted bygenerate_codeasconstraints.ttlalongside the domain model (loadable viastardog icv add <db> constraints.ttl).
Error Conditions
| ID | Trigger | Handling |
|---|---|---|
| ERR-105-001 | Ontology fails to parse (bad Turtle) | raise OntologyParseError(BaseError) with file + cause |
| ERR-105-002 | Object property with no rdfs:range |
skip relationship + warn (cannot type the target) |
| ERR-105-003 | Unknown xsd datatype | map to str + warn |
| ERR-105-004 | Output path escapes the target root | reject (sandbox, shared with SPEC-103) |
| ERR-105-005 | Stardog connection unavailable | StardogConnection raises StardogConnectionError, wrapped by the repository as RepositoryError (both inherit core.base.error.BaseError); generation is offline and unaffected |
API Contracts
def parse_ontology(ttl_path_or_text: str | Path) -> DomainModel: ... # FR-001
def render_ontology(model: DomainModel) -> str: ... # FR-002 (TTL)
def generate_code(model: DomainModel, out_dir: str | Path) -> list[Path]: ... # FR-004
class StardogTemporalRepository: # generated per entity; insert-only, bitemporal
def insert(self, entity) -> None: ...
def supersede(self, iri, **changes) -> None: ... # insert new + tombstone
def as_of(self, iri, instant) -> dict: ... # SPARQL-star point-in-time
Security Requirements
- SEC-105-001: Code/TTL writes confined to the target root; traversal rejected (shared sandbox with SPEC-103).
- SEC-105-002: Generated SPARQL is parameterized/escaped; no string-built injection from ontology IRIs.
- SEC-105-003: No destructive SPARQL (
DELETE/DROP) is ever generated (enforced by template + a generation-time check).
Dependencies
| Component | Spec/Pkg | Purpose |
|---|---|---|
| Logging & Exceptions | SPEC-100 | Generated code's logging + error hierarchy |
| Jinja2 | jinja2>=3.1 (present) |
Deterministic templates |
| rdflib | rdflib>=7 (added) |
Parse/serialize OWL/Turtle, RDF-star |
| Stardog | external (SPARQL 1.1 + RDF-star/SPARQL-star) | Temporal insert-only persistence |
| Refactor tools | SPEC-103 | (optional) the refactor agent may invoke generation as a tool |
Testing Requirements
- TST-105-001: parse a sample
.ttl→ DomainModel with expected entities/fields/relationships/inheritance. - TST-105-002:
render_ontology→ re-parse round-trips structurally (FR-003). - TST-105-003:
generate_codeis deterministic — two runs on the same IR produce identical bytes (FR-006); generated.pycompiles (py_compile). - TST-105-004: generated modules reference
get_loggerand a domain error class; noprint/DELETE/DROPin output (FR-005/SEC-105-003). - TST-105-005: the generated repository emits only INSERT (RDF-star annotated) SPARQL with
txFrom/validFrom/validTo; a change produces insert + tombstone (FR-006). - TST-105-006: add an entity+relationship to the IR → both TTL and code reflect it (FR-007).
- TST-105-007: xsd→python type mapping + cardinality (functional → single, else many).
- (Stardog round-trip is an on-demand integration test; unit tests assert the generated SPARQL shape, no live server, no mocks.)
Definition of Done
- [x]
orchestrata_ultimate.domaingen: IR models + rdflib ontology reader + TTL renderer + deterministic Jinja2 code generator + temporal insert-only Stardog repository template. - [x] Bidirectional round-trip (ontology↔IR) + idempotent codegen verified (
test_ontology_round_trip,test_generate_is_deterministic_and_compiles). - [x] Generated code uses SPEC-100 logging/exceptions (
core.logging.get_logger,BaseError); emits only insert-only RDF-star SPARQL (build_insert/supersede/build_as_of/build_as_recorded/build_as_valid); a generation-time guard rejectsDELETE/DROP/CLEAR. - [x] Real Stardog HTTP SPARQL connection adapter (
StardogConnection) + ascripts/domaingen.pyCLI (parse → generate /--render-ttlround-trip). - [x] SHACL constraint generation for Stardog ICV (
generate_shacl): NodeShape/property shapes with datatype/class/min-maxCount + inheritance viash:node, emitted asconstraints.ttl(FR-008). - [x]
@traces→REQ-105-*; 12 unit tests (TST-105-*) green via the threadpool runner; test-review clean; registered in SPEC_INVENTORY. - [ ] Live Stardog round-trip is an on-demand integration test (no Stardog server in this environment; the unit suite asserts the generated SPARQL/HTTP shape).
Revision History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1.0 | 2026-05-28 | Mohamed | Initial spec — ontology-driven, bidirectional, temporal insert-only domain-model generation |