Tracked design questions and unresolved issues for the RL2 ontology, semantics, and tooling.
Status: Resolved
Affects: rl2.ttl, RL2_Semantics.md, RL2_Vocabulary.md, use cases
Related: ODRL 2.2 odrl:includedIn, RL2_Semantics.md lines 640-654
RL2 had no defined mechanism for expressing action hierarchies. The ontology declared rl2:Action as an OWL class and said profiles define actions as individuals of that class, but provided no property for relating actions hierarchically.
Use cases used rdfs:subClassOf on action individuals (OWL punning), which was confusing for authors, fragile for tooling, and inconsistent with the "actions are individuals" design.
The semantics document referenced three possible mechanisms (rdfs:subClassOf, skos:broader, skos:narrower) without committing to one.
Are actions individuals, classes, or both? And how are action hierarchies expressed?
| Approach | Mechanism | Pros | Cons |
|---|---|---|---|
| Individuals + dedicated property (ODRL style) | rl2:includedIn |
Clean separation; no punning; ODRL-compatible; simple SPARQL queries | Requires defining a new property; subsumption is not RDFS-entailed |
| Classes (pure OWL) | rdfs:subClassOf |
Standard RDFS entailment; reasoners work out of the box | Actions-as-classes is semantically odd; requires punning in norms |
| SKOS concepts | skos:broader / skos:narrower |
Well-established hierarchy pattern | Imports SKOS dependency; transitivity subtleties; conflates concept taxonomy with operational semantics |
| OWL punning (de facto before fix) | a rl2:Action + rdfs:subClassOf |
Works in OWL 2 | Confusing for authors; tooling support varies; semantically muddled |
Actions are named individuals (type-symbols representing kinds of actions, not concrete events). Action hierarchies are expressed via rl2:includedIn, a transitive object property in RL2 Core.
Rationale:
- No punning. Actions remain pure individuals of
rl2:Action. No IRI is both instance and class. - Deterministic evaluation. Action subsumption is a bounded graph reachability problem, not OWL reasoning. Evaluators do explicit traversal; no reasoner required.
- ODRL compatibility. Mirrors
odrl:includedInstructurally, simplifying translation/transpilation. RL2 omits the SKOS dependency (optional alignment available separately). - Clean author story. "Define actions as individuals; relate them with
rl2:includedIn."
rl2.ttl -- Added rl2:includedIn:
rl2:includedIn a owl:ObjectProperty, owl:TransitiveProperty ;
rdfs:domain rl2:Action ;
rdfs:range rl2:Action ;
rdfs:label "Included In" ;
rdfs:comment """Action taxonomy: A includedIn B means A is a narrower action
included within B. Transitive. Do NOT use rdfs:subClassOf for action hierarchies.""" .RL2_Semantics.md -- Replaced vague RDF grounding with:
action_match(x_req, x_pol) := (x_req = x_pol) ∨ reachable(x_req, rl2:includedIn, x_pol)
SPARQL: ASK { ?x_req rl2:includedIn* ?x_pol }. Usage of rdfs:subClassOf for action refinement is non-normative.
RL2_Vocabulary.md -- Documented rl2:includedIn in the Action class notes and property reference table.
CLAUDE.md -- Added common mistake: "Using rdfs:subClassOf for action hierarchies → Use rl2:includedIn".
usecases/no-ml-training.md -- Replaced rdfs:subClassOf with rl2:includedIn:
ai:createEmbeddings a rl2:Action ;
rdfs:label "Create Embeddings" ;
rl2:includedIn ai:trainModel .usecases/internal-use-only.md -- Removed bogus rdfs:subClassOf rl2:Action from action individuals.
usecases/role-hierarchy.md -- Clarified that asset/role hierarchies use rdfs:subClassOf while action hierarchies use rl2:includedIn.
For environments that want SKOS tooling interop, a separate alignment module can be created:
# rl2-skos.ttl (optional, not core)
rl2:includedIn rdfs:subPropertyOf skos:broaderTransitive .
rl2:Action rdfs:subClassOf skos:Concept .This keeps Core lean while enabling SKOS-aware UI tooling.
Be strict in profiles (no punning), tolerant in ingestion: if legacy data contains ?a rdfs:subClassOf ?b where both are rl2:Action individuals, treat it as a repairable authoring error and transpile to ?a rl2:includedIn ?b with a diagnostic.
Status: Resolved
Affects: RL2_Semantics.md
Related: Issue 1 (rl2:includedIn)
Action subsumption via rl2:includedIn applies to request matching but not to duty fulfillment. This creates an asymmetry:
Request matching (line 645) uses subsumption:
matches(Norm(a, x, s, c), R) =
... (x = x_req ∨ x_req ⊑ x) ...
A privilege on trainModel covers a request to fineTune because fineTune includedIn trainModel.
Duty fulfillment (line 1278) checks exact action:
Σ.ObligationState(d) = Active
Σ.Performed(d.subject, d.action, d.object) = true
A duty requiring trainModel is NOT fulfilled by performing fineTune, because the event log records Performed(a, fineTune, s) and the check looks up Performed(a, trainModel, s) — an exact match that fails.
# Action hierarchy
ai:fineTune a rl2:Action ; rl2:includedIn ai:trainModel .
# Privilege: "Licensee may train models"
ex:trainPrivilege a rl2:Privilege ;
rl2:action ai:trainModel .
# Duty: "Licensee must train a model before deadline"
ex:trainDuty a rl2:Duty ;
rl2:action ai:trainModel .Agent performs fineTune:
- Privilege check: PERMIT (fineTune ⊑ trainModel, subsumption applies)
- Duty fulfillment: NOT FULFILLED (Performed looks up trainModel exactly, finds nothing)
Both interpretations are defensible:
Subsumption should apply to duties too:
- A duty to "train a model" is naturally satisfied by fine-tuning (a kind of training)
- The asymmetry is surprising — if the action hierarchy says fineTune IS a kind of trainModel, that should hold everywhere
- Parallel with request matching: if fineTune is "close enough" to trainModel for permission, it should be "close enough" for fulfillment
Exact match for duties is intentional:
- Duties are more specific obligations — "train a model" might mean full training, not just fine-tuning
- Narrower actions may not satisfy the full intent of a broader duty
- Prohibitions would also need consideration: if
fineTuneis performed, is a prohibition ontrainModelviolated? Subsumption says yes, which seems correct — but the samePerformedcheck applies
The prohibition violation rule (line 1099-1102) also uses Performed:
ProhibitionActive(a, x, s, c)
Σ.Performed(a, x, s) = true
Under exact match: performing fineTune does NOT violate a prohibition on trainModel. This seems clearly wrong — if fineTune includedIn trainModel, then fine-tuning should violate a training prohibition.
This strengthens the case that Performed checks should be subsumption-aware, at least for prohibitions.
-
Make
Performedsubsumption-aware: When recordingPerformed(a, fineTune, s), also recordPerformed(a, trainModel, s)for all ancestors in theincludedInchain. Simple but changes the state model. -
Make fulfillment/violation checks subsumption-aware: Change duty fulfillment and prohibition violation checks to use
∃x' : Performed(a, x', s) ∧ (x' = x ∨ x' ⊑ x). KeepsPerformedexact but adds traversal at check time. -
Distinguish by norm type: Privileges and prohibitions use subsumption; duties require exact match. Document this as intentional. Requires a clear rationale for the asymmetry.
-
Leave to profiles: Profiles that define action hierarchies are responsible for specifying whether subsumption applies to fulfillment. Core semantics remain exact-match.
Action subsumption applies uniformly across all norm types. Σ.Performed remains the raw log of exact actions. A subsumption-aware helper performed() is used at query time:
performed(a, x, s, Σ) :=
∃x' : Σ.Performed(a, x', s) ∧ (x' = x ∨ x' ⊑ x)
This is the same bounded graph traversal already required for request matching — no additional reasoning complexity.
Rationale: The prohibition case is decisive. If fineTune includedIn trainModel, performing fine-tuning must violate a prohibition on training. The same logic extends to duty fulfillment: fine-tuning satisfies a duty to train. If a profile needs exact-match duty semantics, it should define the action at the exact level required rather than relying on the absence of subsumption.
All Σ.Performed(a, x, s) checks in RL2_Semantics.md replaced with performed(a, x, s, Σ):
- Request matching section: Defined
performed()alongsideaction_match()and the⊑notation - Duty fulfillment (small-step rule):
performed(a,x,s,Σ) = true - Duty violation (small-step rule):
performed(a,x,s,Σ) = false - Prohibition violation:
performed(a, x, s, Σ) = true - contentHolds:
performed(a, x, s, Σ)forActioncontent - D-FULFILL / D-VIOLATE (big-step rules): Both use
performed() - updateOneDuty (algorithmic form): Uses
performed()
Σ.Performed is unchanged — it still records exact actions via processEvent. performed() adds subsumption at read time.