Create a unit from Ontology and DSL
The model describes classes and fields; a .unit surface defines operations and behaviour. Composer combines them into IR, a plan, C# code and an exposition. The TODO example is a complete input pair, not a customer model. It uses the Composer combination with SDK 1.12.0 and conformance 0.56.0.
Requirements and files
You need access to a Composer release, its NuGet dependencies, .NET matching that release’s global.json, and the reasoner’s Java and Maven. The reviewed .NET configuration names 10.0.203 with rollForward latestFeature. How to obtain Composer: information to be supplied.
Save the following todo.ttl and todo.unit in a separate directory. createdAt comes from the framework; do not add a competing declaration. In this model listName is a field supplied at create, not host configuration.
@prefix todo: <https://seeraos.dev/ns/ontology-unit/todo#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# The todo list, as an ontology. One aggregate, four slots, nothing else — the point of the fixture is that a unit
# somebody wrote by hand can be stated as a vocabulary and generated back, so it is deliberately faithful to that
# unit's read model rather than to what a richer todo domain might look like.
#
# `createdAt` is NOT declared here on purpose: the framework stamps it on every aggregate and the read model
# carries it, so a vocabulary that named its own would only collide with the reserved audit member and have its
# own slot renamed.
#
# `listName` is a slot here and is configuration in the hand-written unit: the same value rides every row on the
# wire, stamped from the unit's settings. A generated unit has nowhere to stamp it from, so the caller supplies it
# on create. That difference is real and is the reason this file cannot make the two units byte-identical.
<https://seeraos.dev/ns/ontology-unit/todo> a owl:Ontology ;
rdfs:label "Todo fixture" ;
rdfs:comment "A todo list: a titled item that is done or not, stamped with when it was made." .
todo:id a owl:DatatypeProperty ; rdfs:label "id" .
todo:title a owl:DatatypeProperty ; rdfs:label "title" ;
rdfs:comment "What the item says." .
todo:done a owl:DatatypeProperty ; rdfs:label "done" ;
rdfs:comment "Whether it has been completed." .
todo:listName a owl:DatatypeProperty ; rdfs:label "listName" ;
rdfs:comment "Which list the item belongs to." .
todo:Todo a owl:Class ;
rdfs:label "Todo" ;
rdfs:comment "One item on the list." ;
rdfs:subClassOf
[ a owl:Restriction ; owl:minCardinality 1 ; owl:onProperty todo:id ],
[ a owl:Restriction ; owl:maxCardinality 1 ; owl:onProperty todo:id ],
[ a owl:Restriction ; owl:allValuesFrom xsd:anyURI ; owl:onProperty todo:id ],
[ a owl:Restriction ; owl:minCardinality 1 ; owl:onProperty todo:title ],
[ a owl:Restriction ; owl:maxCardinality 1 ; owl:onProperty todo:title ],
[ a owl:Restriction ; owl:allValuesFrom xsd:string ; owl:onProperty todo:title ],
[ a owl:Restriction ; owl:minCardinality 1 ; owl:onProperty todo:done ],
[ a owl:Restriction ; owl:maxCardinality 1 ; owl:onProperty todo:done ],
[ a owl:Restriction ; owl:allValuesFrom xsd:boolean ; owl:onProperty todo:done ],
[ a owl:Restriction ; owl:minCardinality 1 ; owl:onProperty todo:listName ],
[ a owl:Restriction ; owl:maxCardinality 1 ; owl:onProperty todo:listName ],
[ a owl:Restriction ; owl:allValuesFrom xsd:string ; owl:onProperty todo:listName ] .
unit todo contract todo.v1
expose Todo { crud, lifecycle }
expose graph { query, update }
emits TodoNotification.raised
emits TodoNotification.changed
expose Todo.complete sets done = true announces completed
expose Todo.reopen sets done = false announces reopened
rule todo-created-notice on Todo.created {
emit TodoNotification.raised { todoId = trigger.id, title = trigger.title, done = trigger.done, listName = trigger.listName }
}
rule todo-updated-notice on Todo.updated {
emit TodoNotification.changed { todoId = trigger.id, title = trigger.title, done = trigger.done, listName = trigger.listName }
}
rule todo-completed-notice on Todo.completed {
emit TodoNotification.changed { todoId = trigger.id, title = trigger.title, done = trigger.done, listName = trigger.listName }
}
Model to IR
COMPOSER_DIR names the selected release sources; CASE_DIR names a fresh input directory. The reasoner resolves the model, checks consistency and writes IR. Imports must match that release configuration.
cd "$COMPOSER_DIR"
mvn -q -f reasoner/pom.xml package
java -jar reasoner/target/reasoner.jar --ontology "$CASE_DIR/todo.ttl" --out "$CASE_DIR/todo.ir.json"
Plan and generation
Expected outputs are a code directory, exposition.json and a plan. Identity flags must match the surface header. Do not mix historical .eca syntax into it.
dotnet run --project tools/SeeraOS.Units.Ontology.Generator -- --ir "$CASE_DIR/todo.ir.json" --surface "$CASE_DIR/todo.unit" --out "$CASE_DIR/generated-todo" --unit GeneratedTodo --namespace Demo.Todo --segment todo --plan-out "$CASE_DIR/todo.plan.json"
Host and updates
In a separate Composer copy, use the provided host while preserving its names and dependencies. --unit-dir emits registration materials but does not create a complete host project by itself.
dotnet run --project tools/SeeraOS.Units.Ontology.Generator -- --ir "$CASE_DIR/todo.ir.json" --surface "$CASE_DIR/todo.unit" --out generated/SeeraOS.Units.Generated.Ontology/src --unit GeneratedOntology --namespace SeeraOS.Units.Generated.Ontology --segment ontology
dotnet build generated/SeeraOS.Units.Generated.Ontology
After model or surface changes, regenerate and inspect plan and exposition diffs. Do not edit .g.cs as an independent domain source. Data schema changes need a separate migration plan.
The host needs standard lifecycle, identity, OIDC, Dapr and Control Plane configuration. /.well-known/seeraos-exposition exposes parity; compare the appropriate document in the list with the release artifact. Host RPC uses POST /seeraos-rpc/{entity}/{operation}; Edge routes come from its own projection.
Model inconsistency points to cardinalities and imports. Unknown types or fields point to the model+surface pair. Identity conflicts point to headers and flags. Configuration and auth failures belong to runtime, covered in troubleshooting. Documented generator results do not need rerunning by the documentation author.