domaingen package

Reviewed against commit 8a2cb42ad on 2026-10-23.

Reference: package page (7 modules, 738 lines) · modules: models, reader, ttl, generator, constraints, stardog. The package is self-contained; see the overview for where generated projects fit. Reviewed with no factual drift: the seven modules and their roles match the code.

The story

orchestrata_ultimate.domaingen exists to answer a deceptively simple question posed by SPEC-105: what if the domain model of a generated project were not hand-written code at all, but a formal ontology? In the orchestrata pipeline, agents scaffold and evolve codebases; this package gives them a way to derive an entire screaming-architecture / modular-monolith domain layer — entities, repositories, package structure — from an OWL/Turtle ontology, deterministically, so the same ontology always yields byte-identical code. The scripts/domaingen.py CLI is the thin front door; everything of substance lives here.

The heart of the design is a canonical intermediate representation defined in models.py: pure dataclasses (DomainModel, Module, Entity, Relationship, Field) that are the single source of truth. Ontology and code are both views of this IR. A request flows in one of two directions around it. Coming from the semantic side, reader.py's parse_ontology uses rdflib to turn OWL/Turtle classes, datatype properties, object properties, and OWL restrictions into the IR, sorting entities, fields and relationships by name so everything downstream is stable. Going back out, ttl.py's render_ontology projects the IR to Turtle with stable prefixes and no timestamps, which is what makes the round-trip parse_ontology(render_ontology(ir)) structurally identity-preserving (REQ-105-004/026).

The third projection is code. generator.py's generate_code renders one package per IR module — entities.py, repository.py, __init__.py — from the Jinja2 templates in templates/, topologically ordering entities parents-before-children with a name tiebreak for byte-identical output. It also embeds two safety properties: all writes are sandboxed to the target root, and a generation-time guard rejects any destructive SPARQL in the generated persistence layer (SEC-105-003), because the generated repositories are insert-only, bitemporal RDF-star stores. Those repositories talk to a live triple store through stardog.py's StardogConnection, a minimal real HTTP SPARQL 1.1 adapter — the generated code only requires an object with update/query, and this class satisfies that contract. Completing the loop, constraints.py's generate_shacl emits SHACL shapes from the same IR for Stardog Integrity Constraint Validation, so the data the generated code writes can be validated against the very ontology it came from.

The package leans on core for its shared plumbing — every error type (OntologyParseError, CodeGenError, StardogConnectionError) inherits core.base.error.BaseError, and the public functions carry @traces decorators from core.traceability linking them to their SPEC-105 requirements. Beyond that, domaingen is deliberately self-contained: no other orchestrata subsystem is imported, and its only external dependencies are rdflib and Jinja2. Public API via __init__.py: parse_ontology, render_ontology, generate_code, generate_shacl, plus the IR dataclasses and error types.

from orchestrata_ultimate.domaingen import parse_ontology, render_ontology, generate_code
model = parse_ontology("ontology.ttl")
generate_code(model, "build/domain")

Key responsibilities

Class diagram

Everything in the package orbits the IR dataclasses in models.py. DomainModel composes Modules, each Module composes Entitys, and each Entity composes its Fields (datatype properties → typed attributes) and Relationships (object properties → edges to other entities). The reader builds this tree; the Turtle renderer, the code generator, and the SHACL emitter each read it without ever bypassing it (generated from the AST, render-verified):

Diagram projects-orchestrata-ultimate-modules-domaingen-1

The only stateful class outside the IR is StardogConnection — the SPARQL-over-HTTP adapter the generated repositories talk to. Alongside it sit the package's three error types, all thin subclasses of core's shared BaseError (generated from the AST, render-verified):

Diagram projects-orchestrata-ultimate-modules-domaingen-2

StardogConnection keeps base_url/database and a lazily-created httpx-like client (_http); update POSTs insert-only SPARQL-star to <base>/<db>/update and query POSTs to <base>/<db>/query, each wrapping any HTTP failure in StardogConnectionError (ERR-105-005). OntologyParseError fires when the reader cannot parse a source (ERR-105-001); CodeGenError fires on a sandbox escape or a forbidden-SPARQL guard trip.

Module dependency diagram

Internal module imports within orchestrata_ultimate.domaingen (generated from the AST, render-verified):

Diagram projects-orchestrata-ultimate-modules-domaingen-3

Module inventory

Module Purpose
domaingen/__init__.py Package facade — re-exports the public API (parse/render/generate/SHACL, IR types, errors).
domaingen/models.py Domain-model IR (SPEC-105) — canonical intermediate representation as pure dataclasses; single source of truth for both ontology and code views.
domaingen/reader.py Ontology (OWL/Turtle) → DomainModel IR via rdflib (FR-001); deterministic (sorted names).
domaingen/ttl.py DomainModel IR → Turtle ontology (FR-002/003); deterministic output so parse_ontology(render_ontology(ir)) round-trips structurally.
domaingen/generator.py DomainModel IR → screaming-architecture domain code (FR-004/005/006); deterministic Jinja2 codegen, sandboxed writes, destructive-SPARQL guard.
domaingen/constraints.py IR → SHACL constraint shapes for Stardog ICV (FR-008); NodeShape per entity, PropertyShape per field/relationship.
domaingen/stardog.py Minimal Stardog HTTP SPARQL connection (FR-006 / REQ-105-042) — the real adapter the generated repositories accept.
domaingen/templates/ Jinja2 templates used by the generator: entities.py.j2, repository.py.j2, init.py.j2.

models.py — the canonical IR

Everything in the package revolves around this module. It defines the IR as pure dataclasses — Field (a datatype property become a typed attribute), Relationship, Entity, Module, and the top-level DomainModel — plus the XSD_TO_PY mapping that translates XSD datatype local names into the Python types used in generated code (REQ-105-018), covering strings, the integer family, decimals/doubles, booleans, and the date/time types. DomainModel exposes the single convenience method all_entities(), which flattens the module tree into a flat entity list that the renderer, generator, and SHACL emitter all iterate. Because the IR is the single source of truth, the reader builds it, and both the Turtle renderer and the code generator merely project it; neither projection ever bypasses these dataclasses, which is what keeps the ontology view and the code view guaranteed-consistent (FR-001..007).

reader.py — ontology to IR

parse_ontology(source) -> DomainModel is the ingestion side. It loads OWL/Turtle with rdflib and walks the graph through a series of internal passes — _parse_graph, _build_entities, _attach_datatype_properties, _attach_object_properties, and _apply_restrictions, which turns OWL restrictions into required properties, with _namespace_of grouping classes into IR modules. Datatype properties become Fields (their XSD range mapped through XSD_TO_PY, unknown types warned and defaulted to str); object properties become Relationships (a FunctionalProperty sets many=False, owl:inverseOf fills inverse, a missing range is warned and skipped). Anything unparseable raises OntologyParseError, a BaseError subclass traced to ERR-105-001. Determinism is a design commitment here rather than an accident: entities, fields and relationships are sorted by name so that the IR — and therefore everything generated from it — is stable across runs.

ttl.py — IR back to Turtle

render_ontology(model) -> str is the inverse of the reader. It emits classes and properties in sorted order with stable prefixes and no timestamps, so a model that goes out through this module and back in through parse_ontology is structurally identical (REQ-105-004/026). Internally it splits the work across _render_class, _render_datatype_properties, and _render_object_properties, and maintains _PY_TO_XSD, the inverse of the reader's XSD_TO_PY mapping — the two modules deliberately bracket the IR from opposite sides. constraints.py also borrows _PY_TO_XSD when emitting SHACL datatypes, which is why the dependency diagram shows constraints → ttl.

generator.py — IR to domain code

generate_code(model, out_dir) -> list[Path] turns the IR into a living codebase. For each IR module it renders a package containing entities.py, repository.py and __init__.py from the templates/ Jinja2 files, then writes an ontology-wide constraints.ttl from generate_shacl. _topo_sort orders entities parents-before-children with a name tiebreak so output is byte-identical across runs, and _safe confines every write to the target root so a hostile ontology cannot escape the output sandbox. Because the generated repositories are insert-only by contract, a generation-time guard scans for destructive SPARQL update statements (DELETE DATA, DELETE WHERE, DROP GRAPH, CLEAR ALL, and friends in _FORBIDDEN_SPARQL) and refuses to emit them (SEC-105-003) — matching actual update syntax, not words in prose or docstrings. Sandbox escapes and guard trips both raise CodeGenError.

constraints.py — SHACL for Stardog ICV

generate_shacl(model) -> str renders SHACL shapes straight from the IR for Stardog Integrity Constraint Validation (FR-008). It emits one sh:NodeShape per entity (sh:targetClass), a sh:property PropertyShape per field (sh:datatype via _PY_TO_XSD, sh:maxCount 1, plus sh:minCount 1 when required) and per relationship (sh:class on the target, sh:minCount 1 when required, sh:maxCount 1 when single-valued), and an sh:node :ParentShape link so a subclass also satisfies its parent's shape. Output is sorted and timestamp-free, so the constraints round-trip with the ontology and the generated code. The result is loadable with stardog icv add <db> constraints.ttl.

stardog.py — the persistence adapter

StardogConnection is the minimal real SPARQL-1.1-over-HTTP adapter the generated repositories accept — they only require an object with update/query, and this class satisfies that contract (FR-006 / REQ-105-042). It stores base_url and database, derives update_url/query_url as properties, and lazily builds an httpx client (or accepts an injected one via the http= seam, so unit tests assert URL/auth construction with no network and no mocks). update POSTs an insert-only SPARQL-star update; query POSTs a query with an application/sparql-results+json Accept header and returns the JSON bindings. Any HTTP failure is wrapped in StardogConnectionError (ERR-105-005).

Class map (generated)

Generated from the live source AST by scripts/gen-module-class-diagrams.py — do not hand-edit. Abstract classes have a green border; members listed are those defined on the class itself (inherited members excluded). Solid empty-triangle arrows = in-package inheritance; dashed arrows into dashed boxes = external/stdlib/cross-package bases; filled diamond = composition (constructed in __init__); open diamond = aggregation (annotated/injected reference); diamond edge labels name the owning attribute.

domaingen class map