feat(uko): add UKO ontology scaffolding
CI / lint (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 15s
CI / security (pull_request) Successful in 30s
CI / typecheck (pull_request) Successful in 35s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 14s
CI / unit_tests (pull_request) Successful in 2m8s
CI / integration_tests (pull_request) Successful in 3m36s
CI / docker (pull_request) Successful in 39s
CI / coverage (pull_request) Successful in 5m19s
CI / lint (push) Successful in 14s
CI / typecheck (push) Successful in 49s
CI / quality (push) Successful in 16s
CI / security (push) Successful in 1m15s
CI / build (push) Successful in 14s
CI / unit_tests (push) Successful in 3m25s
CI / integration_tests (push) Successful in 4m53s
CI / benchmark-regression (push) Has been skipped
CI / coverage (push) Successful in 6m6s
CI / benchmark-publish (push) Successful in 15m16s
CI / docker (push) Successful in 39s
CI / benchmark-regression (pull_request) Successful in 34m22s

Add UKO Layer 0-3 ontology skeleton (RDF/TTL) with base URI, version IRI,
and prefix conventions. Python loaders handle URI parsing, inheritance
resolution, versioning metadata, and validation of undefined prefixes and
missing rdf:type. Includes Behave BDD scenarios, Robot Framework integration
tests, ASV benchmarks, and reference documentation.

- Resolve rdfs:domain and rdfs:range values to full URIs in parser
- Fix type: ignore in Robot helper with proper Callable typing
- Export UKO models from core/__init__.py
- Add parent-URI existence validation in validate()
- Rename scenario to clarify parser-skips vs validation-rejects
- Add scenario testing validation with injected typeless node
- Add Scenario Outline for multi-layer node count assertions

- TTL: Layer 0 classes (InformationUnit, Container, Atom, Annotation,
  Boundary) with contains/references/dependsOn properties. Layer 1
  uko-code: classes (Module, Callable, TypeDefinition, TestCase, Import)
  with hasReturnType/hasParameters/testsCallable. Layer 2 uko-oo: classes
  (Class, Interface, Method, Attribute) with inheritsFrom/implements and
  rdfs:subPropertyOf. New URI scheme cleveragents.ai/ontology/uko#.
- TTL: uko-oo:Class has dual rdfs:subClassOf (TypeDefinition + Container),
  uko-oo:Interface has dual rdfs:subClassOf (TypeDefinition + Boundary).
- Model: UKONode.parent_uri replaced with parent_uris: tuple[str, ...]
  to support multi-parent rdfs:subClassOf.
- Loader: prefix regex supports hyphenated names (uko-code, uko-oo).
  Semantic domain prefix maps (_LAYER_PREFIXES, _LAYER_IRI_PREFIXES).
  Comma-separated rdfs:subClassOf parsing. rdfs:domain/range/subPropertyOf
  URI resolution. BFS DAG traversal with DFS cycle detection for
  resolve_inheritance. HTTP URI skip in validation (D-1 fix). Duplicate
  error guard (B-1 fix). Consistent quoting (M-1 fix).
- Services __init__: export UKOLoader and UKOValidationError.
- Tests: 24 Behave scenarios (multi-parent, domain/range resolution,
  non-existent parent validation). 4 Robot smoke tests. Consolidated
  context attributes (T-3 fix).
- Docs: uko.md written for spec-aligned structure.

- Namespace prefix match in parent-URI validation includes a
delimiter guard (# or /) preventing false positives on URIs that share
the UKO prefix string (e.g. ukobogus:, ukoo:).

ISSUES CLOSED: #189
This commit was merged in pull request #466.
This commit is contained in:
khyari hamza
2026-02-27 11:43:34 +00:00
committed by Luis Mendes
parent 4ca4874c4d
commit ad53a659de
12 changed files with 2076 additions and 0 deletions
+14
View File
@@ -166,6 +166,20 @@
capability summary fields, tool counts, and description columns. Added `--format json/yaml`
schemas for refresh output. Updated CLI reference documentation with refresh examples and
caching behavior. (#167)
- Added UKO Layer 0-3 ontology scaffolding (RDF/TTL) aligned with specification
Section 14. Layer 0 (`uko:`) defines InformationUnit, Container, Atom,
Annotation, Boundary plus contains/references/dependsOn relationships,
content properties (hasRendering, renderingDepth, hasFullContent),
provenance properties (sourceResource, sourcePath, sourceRange), and
temporal properties (validFrom, validUntil, isCurrent, isRevisionOf).
Layer 1 (`uko-code:`) defines Module, Callable, TypeDefinition, TestCase,
Import plus hasReturnType/hasParameters/testsCallable. Layer 2 (`uko-oo:`)
defines Class, Interface, Method, Attribute plus inheritsFrom/implements
with `rdfs:subPropertyOf`. Layer 3 is reserved for DetailLevelMap
insertions. Loader supports semantic domain prefixes, hyphenated prefix
names, full-URI layer detection, multi-parent `rdfs:subClassOf` (DAG
traversal via BFS), `rdfs:domain`/`rdfs:range`/`rdfs:subPropertyOf`
resolution, and non-existent parent validation. (#189)
- Added `AgentSkillSpec` loader that parses SKILL.md frontmatter and progressive disclosure
sections into structured `SkillStep` objects with stable 1-based ordering. Supports
namespaced naming (`namespace/short_name`), optional `steps`, `version`, `compatibility`,
+80
View File
@@ -0,0 +1,80 @@
"""ASV benchmarks for UKO ontology loader performance.
Suites:
- UKOLoadSuite: time_load_ttl, time_parse_prefixes, time_parse_nodes
- UKOValidationSuite: time_validate_ontology, time_resolve_inheritance
- UKOLayerSuite: time_get_layer_0, time_get_layer_3
"""
from __future__ import annotations
import importlib
import sys
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.application.services.uko_loader import UKOLoader # noqa: E402
_TTL_PATH = Path(__file__).resolve().parents[1] / "docs" / "ontology" / "uko.ttl"
_TTL_TEXT = _TTL_PATH.read_text(encoding="utf-8")
class UKOLoadSuite:
"""Benchmark TTL loading and parsing throughput."""
def setup(self) -> None:
self.loader = UKOLoader()
def time_load_ttl(self) -> None:
"""Benchmark loading uko.ttl from disk."""
self.loader.load(_TTL_PATH)
def time_parse_prefixes(self) -> None:
"""Benchmark prefix-line parsing."""
self.loader._parse_prefixes(_TTL_TEXT)
def time_parse_nodes(self) -> None:
"""Benchmark full string parsing."""
self.loader.load_from_string(_TTL_TEXT)
class UKOValidationSuite:
"""Benchmark validation and inheritance resolution."""
def setup(self) -> None:
self.loader = UKOLoader()
self.ontology = self.loader.load(_TTL_PATH)
def time_validate_ontology(self) -> None:
"""Benchmark ontology validation."""
self.loader.validate(self.ontology)
def time_resolve_inheritance(self) -> None:
"""Benchmark uko-oo:Class inheritance chain resolution."""
self.loader.resolve_inheritance(
self.ontology,
"https://cleveragents.ai/ontology/uko/oo#Class",
)
class UKOLayerSuite:
"""Benchmark layer-filtered node retrieval."""
def setup(self) -> None:
self.loader = UKOLoader()
self.ontology = self.loader.load(_TTL_PATH)
def time_get_layer_0(self) -> None:
"""Benchmark Layer 0 node retrieval."""
self.loader.get_layer_nodes(self.ontology, 0)
def time_get_layer_3(self) -> None:
"""Benchmark Layer 3 node retrieval."""
self.loader.get_layer_nodes(self.ontology, 3)
+223
View File
@@ -0,0 +1,223 @@
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
@prefix uko-code: <https://cleveragents.ai/ontology/uko/code#> .
@prefix uko-oo: <https://cleveragents.ai/ontology/uko/oo#> .
@prefix uko-py: <https://cleveragents.ai/ontology/uko/py#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# ===========================================================================
# Ontology metadata
# ===========================================================================
uko: a owl:Ontology ;
owl:versionIRI <https://cleveragents.ai/ontology/uko/0.1.0> ;
rdfs:label "Unified Knowledge Ontology" ;
rdfs:comment "CleverAgents knowledge representation layers 0-3" .
# ===========================================================================
# Layer 0: Universal Foundation (uko:)
# ===========================================================================
# -- Base Classes --
uko:InformationUnit a owl:Class ;
rdfs:label "InformationUnit" ;
rdfs:comment "The root of every UKO node. Anything that can appear in an actor's context." .
uko:Container a owl:Class ;
rdfs:subClassOf uko:InformationUnit ;
rdfs:label "Container" ;
rdfs:comment "An information unit that contains other information units (file, module, class, section)." .
uko:Atom a owl:Class ;
rdfs:subClassOf uko:InformationUnit ;
rdfs:label "Atom" ;
rdfs:comment "A leaf-level information unit (function body, paragraph, config value)." .
uko:Annotation a owl:Class ;
rdfs:subClassOf uko:InformationUnit ;
rdfs:label "Annotation" ;
rdfs:comment "Metadata attached to another information unit (comment, docstring, attribute)." .
uko:Boundary a owl:Class ;
rdfs:subClassOf uko:InformationUnit ;
rdfs:label "Boundary" ;
rdfs:comment "An interface point between containers (export, API endpoint, public method signature)." .
# -- Core Relationships --
uko:contains a owl:ObjectProperty ;
rdfs:domain uko:Container ;
rdfs:range uko:InformationUnit ;
rdfs:label "contains" ;
rdfs:comment "Parent contains child (e.g., module contains class)." .
uko:references a owl:ObjectProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko:InformationUnit ;
rdfs:label "references" ;
rdfs:comment "Weak reference (e.g., a function mentions a type in a docstring)." .
uko:dependsOn a owl:ObjectProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko:InformationUnit ;
rdfs:label "dependsOn" ;
rdfs:comment "Strong dependency (e.g., import, inheritance, call)." .
# -- Content Properties --
uko:hasRendering a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:string ;
rdfs:label "hasRendering" ;
rdfs:comment "A rendered text representation of this node at a specific detail depth. Multiple renderings at different depths may exist for the same node. The depth is indicated by the associated uko:renderingDepth value." .
uko:renderingDepth a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:nonNegativeInteger ;
rdfs:label "renderingDepth" ;
rdfs:comment "The integer detail depth at which the associated uko:hasRendering was produced. Paired with uko:hasRendering via a blank node or reification." .
uko:hasFullContent a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:string ;
rdfs:label "hasFullContent" ;
rdfs:comment "Shorthand for the maximum-depth rendering of this node (complete, unabridged content). Equivalent to uko:hasRendering at the domain's maximum depth." .
# -- Provenance Properties --
uko:sourceResource a owl:ObjectProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:label "sourceResource" ;
rdfs:comment "The CleverAgents Resource (by ULID) this node was extracted from." .
uko:sourcePath a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:string ;
rdfs:label "sourcePath" ;
rdfs:comment "Path within the resource (e.g., file path relative to the resource root)." .
uko:sourceRange a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:string ;
rdfs:label "sourceRange" ;
rdfs:comment "Byte or line range within the source file (e.g., '42:1-87:0')." .
# -- Temporal Properties --
uko:validFrom a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:dateTime ;
rdfs:label "validFrom" .
uko:validUntil a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:dateTime ;
rdfs:label "validUntil" .
uko:isCurrent a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:boolean ;
rdfs:label "isCurrent" .
uko:isRevisionOf a owl:ObjectProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko:InformationUnit ;
rdfs:label "isRevisionOf" ;
rdfs:comment "Links a revised node to its predecessor." .
# ===========================================================================
# Layer 1: General Software (uko-code:)
# ===========================================================================
uko-code:Module a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Module" ;
rdfs:comment "A source code module (file, package, namespace)." .
uko-code:Callable a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Callable" ;
rdfs:comment "Any callable unit (function, method, procedure, lambda)." .
uko-code:TypeDefinition a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "TypeDefinition" ;
rdfs:comment "A type or schema definition (class, struct, interface, enum, typedef)." .
uko-code:TestCase a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "TestCase" ;
rdfs:comment "A test case or test function." .
uko-code:Import a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "Import" ;
rdfs:comment "An import/include/require statement." .
# -- Layer 1 Properties --
uko-code:hasReturnType a owl:DatatypeProperty ;
rdfs:domain uko-code:Callable ;
rdfs:range xsd:string ;
rdfs:label "hasReturnType" .
uko-code:hasParameters a owl:DatatypeProperty ;
rdfs:domain uko-code:Callable ;
rdfs:range xsd:string ;
rdfs:label "hasParameters" ;
rdfs:comment "JSON-encoded parameter list." .
uko-code:testsCallable a owl:ObjectProperty ;
rdfs:domain uko-code:TestCase ;
rdfs:range uko-code:Callable ;
rdfs:label "testsCallable" ;
rdfs:comment "Links a test case to the callable it tests." .
# ===========================================================================
# Layer 2: Object-Oriented Paradigm (uko-oo:)
# ===========================================================================
uko-oo:Class a owl:Class ;
rdfs:subClassOf uko-code:TypeDefinition , uko:Container ;
rdfs:label "Class" ;
rdfs:comment "An OO class that contains methods and attributes." .
uko-oo:Interface a owl:Class ;
rdfs:subClassOf uko-code:TypeDefinition , uko:Boundary ;
rdfs:label "Interface" ;
rdfs:comment "An interface or abstract base class." .
uko-oo:Method a owl:Class ;
rdfs:subClassOf uko-code:Callable ;
rdfs:label "Method" ;
rdfs:comment "A method defined within a class." .
uko-oo:Attribute a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Attribute" ;
rdfs:comment "A class or instance attribute." .
# -- Layer 2 Properties --
uko-oo:inheritsFrom a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-oo:Class ;
rdfs:range uko-oo:Class ;
rdfs:label "inheritsFrom" .
uko-oo:implements a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-oo:Class ;
rdfs:range uko-oo:Interface ;
rdfs:label "implements" .
# ===========================================================================
# Layer 3: Technology-Specific Specializations
# ===========================================================================
# Layer 3 extends Layer 2 (or Layer 1) with technology-specific refinements.
# Per the specification, Layer 3 is defined via DetailLevelMap insertions
# rather than new OWL classes. The uko-py: prefix is reserved for
# Python-specific extensions when DetailLevelMap support is added.
+191
View File
@@ -0,0 +1,191 @@
# UKO — Unified Knowledge Ontology
This document describes the **Unified Knowledge Ontology (UKO)**, a layered
RDF/Turtle ontology used by CleverAgents to represent code artifacts,
structural relationships, and language-specific extensions.
## Overview
UKO organises knowledge into four layers, each building on the previous:
| Layer | Prefix(es) | IRI Namespace | Purpose |
|-------|------------|---------------|---------|
| 0 | `uko:` | `https://cleveragents.ai/ontology/uko#` | Universal foundation — base information unit types and core relationships |
| 1 | `uko-code:`, `uko-doc:`, `uko-data:`, `uko-infra:` | `https://cleveragents.ai/ontology/uko/code#` etc. | General software — modules, callables, types, tests, imports |
| 2 | `uko-oo:`, `uko-func:`, `uko-proc:` | `https://cleveragents.ai/ontology/uko/oo#` etc. | Paradigm-specific — OO classes, interfaces, methods, attributes |
| 3 | `uko-py:`, `uko-ts:`, `uko-rs:`, `uko-java:` | `https://cleveragents.ai/ontology/uko/py#` etc. | Technology-specific — DetailLevelMap insertions (no OWL classes) |
The ontology file lives at `docs/ontology/uko.ttl` and uses the base URI
`https://cleveragents.ai/ontology/uko#`.
## URI Format and Prefix Conventions
Every UKO concept is identified by a URI under the CleverAgents ontology
namespace. Layers use semantic domain prefixes rather than numeric indices:
```
https://cleveragents.ai/ontology/uko# # Layer 0 (foundation)
https://cleveragents.ai/ontology/uko/code# # Layer 1 (code domain)
https://cleveragents.ai/ontology/uko/oo# # Layer 2 (OO paradigm)
https://cleveragents.ai/ontology/uko/py# # Layer 3 (Python — reserved)
```
In Turtle files these are abbreviated using `@prefix` declarations:
```turtle
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
@prefix uko-code: <https://cleveragents.ai/ontology/uko/code#> .
@prefix uko-oo: <https://cleveragents.ai/ontology/uko/oo#> .
```
## Layer Descriptions
### Layer 0 — Universal Foundation (`uko:`)
Defines the root types that all other layers extend:
- **InformationUnit** — the root of every UKO node; anything that can appear in an actor's context.
- **Container** — an information unit that contains other units (file, module, class, section).
- **Atom** — a leaf-level information unit (function body, paragraph, config value).
- **Annotation** — metadata attached to another unit (comment, docstring, attribute).
- **Boundary** — an interface point between containers (export, API endpoint, public method signature).
Core relationship properties:
- **contains** — parent contains child (`rdfs:domain` Container, `rdfs:range` InformationUnit).
- **references** — weak reference (e.g. a function mentions a type in a docstring).
- **dependsOn** — strong dependency (import, inheritance, call).
Content properties:
- **hasRendering** — rendered text at a specific detail depth.
- **renderingDepth** — the integer detail depth of the associated rendering.
- **hasFullContent** — shorthand for the maximum-depth rendering.
Provenance properties:
- **sourceResource** — the CleverAgents Resource (by ULID) this node was extracted from.
- **sourcePath** — file path within the resource.
- **sourceRange** — byte or line range within the source file (e.g. `42:1-87:0`).
Temporal properties:
- **validFrom** — timestamp when this node version became valid.
- **validUntil** — timestamp when this node version was superseded.
- **isCurrent** — whether this is the current version.
- **isRevisionOf** — links a revised node to its predecessor.
### Layer 1 — General Software (`uko-code:`)
Structural code concepts that extend Layer 0 classes:
- **Module** — a source code module (subclass of Container).
- **Callable** — any callable unit: function, method, procedure, lambda (subclass of Atom).
- **TypeDefinition** — a type or schema definition (subclass of Atom).
- **TestCase** — a test case or test function (subclass of Atom).
- **Import** — an import/include/require statement (subclass of Annotation).
Layer 1 properties:
- **hasReturnType** — datatype property on Callable.
- **hasParameters** — JSON-encoded parameter list on Callable.
- **testsCallable** — links a TestCase to the Callable it tests.
### Layer 2 — Object-Oriented Paradigm (`uko-oo:`)
OO-specific specialisations of Layer 1 classes:
- **Class** — an OO class (subclass of TypeDefinition).
- **Interface** — an interface or abstract base class (subclass of TypeDefinition).
- **Method** — a method defined within a class (subclass of Callable).
- **Attribute** — a class or instance attribute (subclass of Atom).
Layer 2 properties:
- **inheritsFrom** — `rdfs:subPropertyOf uko:dependsOn`; domain/range both Class.
- **implements** — `rdfs:subPropertyOf uko:dependsOn`; domain Class, range Interface.
Note: `uko-oo:Class` has dual `rdfs:subClassOf` parents
(`uko-code:TypeDefinition` and `uko:Container`), and `uko-oo:Interface`
has dual parents (`uko-code:TypeDefinition` and `uko:Boundary`). The
loader supports multi-parent `rdfs:subClassOf` and resolves inheritance
as a DAG using BFS.
### Layer 3 — Technology-Specific
Per the specification, Layer 3 is defined via DetailLevelMap insertions
rather than new OWL classes. The `uko-py:` prefix is reserved for
Python-specific extensions when DetailLevelMap support is added.
## Versioning
The ontology carries an `owl:versionIRI` that encodes a semver string:
```turtle
uko: a owl:Ontology ;
owl:versionIRI <https://cleveragents.ai/ontology/uko/0.1.0> .
```
The loader extracts the version from the last path segment of the IRI and
validates it against the `SUPPORTED_VERSIONS` registry.
## Extension Points
To add a custom domain or language binding:
1. Define a new `@prefix` (e.g. `uko-go:`) in `uko.ttl`.
2. Declare classes using `rdfs:subClassOf` to connect them to the
appropriate parent in Layers 0-2.
3. Update the `_LAYER_PREFIXES` and `_LAYER_IRI_PREFIXES` maps in
`uko_loader.py` so the parser assigns the correct layer number.
4. Add the new version string to `UKOLoader.SUPPORTED_VERSIONS`.
## Parsing Walkthrough
```python
from pathlib import Path
from cleveragents.application.services.uko_loader import UKOLoader
loader = UKOLoader()
ontology = loader.load(Path("docs/ontology/uko.ttl"))
# List Layer 0 nodes
for node in loader.get_layer_nodes(ontology, layer=0):
print(node.label, node.uri)
# Resolve inheritance for uko-oo:Class
chain = loader.resolve_inheritance(
ontology,
"https://cleveragents.ai/ontology/uko/oo#Class",
)
for node in chain:
print(f" Layer {node.layer}: {node.label}")
```
Output:
```
InformationUnit https://cleveragents.ai/ontology/uko#InformationUnit
Container https://cleveragents.ai/ontology/uko#Container
Atom https://cleveragents.ai/ontology/uko#Atom
Annotation https://cleveragents.ai/ontology/uko#Annotation
Boundary https://cleveragents.ai/ontology/uko#Boundary
contains https://cleveragents.ai/ontology/uko#contains
references https://cleveragents.ai/ontology/uko#references
dependsOn https://cleveragents.ai/ontology/uko#dependsOn
hasRendering https://cleveragents.ai/ontology/uko#hasRendering
renderingDepth https://cleveragents.ai/ontology/uko#renderingDepth
hasFullContent https://cleveragents.ai/ontology/uko#hasFullContent
sourceResource https://cleveragents.ai/ontology/uko#sourceResource
sourcePath https://cleveragents.ai/ontology/uko#sourcePath
sourceRange https://cleveragents.ai/ontology/uko#sourceRange
validFrom https://cleveragents.ai/ontology/uko#validFrom
validUntil https://cleveragents.ai/ontology/uko#validUntil
isCurrent https://cleveragents.ai/ontology/uko#isCurrent
isRevisionOf https://cleveragents.ai/ontology/uko#isRevisionOf
Layer 2: Class
Layer 1: TypeDefinition
Layer 0: Container
Layer 0: Atom
Layer 0: InformationUnit
```
+440
View File
@@ -0,0 +1,440 @@
"""Behave step implementations for UKO ontology feature."""
from __future__ import annotations
from pathlib import Path
from behave import given, then, when
from behave.runner import Context
from cleveragents.application.services.uko_loader import (
UKOLoader,
UKOValidationError,
)
from cleveragents.domain.models.core.uko import (
UKONode,
UKOOntology,
UKOPrefix,
UKOVersion,
)
_TTL_FILE = Path(__file__).resolve().parents[2] / "docs" / "ontology" / "uko.ttl"
# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------
@given("a TTL string")
@given("a TTL string:")
def step_given_ttl_string(context: Context) -> None:
context.ttl_text = context.text
@given("the UKO ontology is loaded from the TTL file")
def step_given_uko_loaded(context: Context) -> None:
loader = UKOLoader()
context.ontology = loader.load(_TTL_FILE)
context.loader = loader
@given("a TTL string with a node missing rdf_type")
@given("a TTL string with a node missing rdf_type:")
def step_given_ttl_missing_rdf_type(context: Context) -> None:
context.ttl_text = context.text
@given("a UKO ontology with an undefined prefix node")
def step_given_undefined_prefix(context: Context) -> None:
context.ontology = UKOOntology(
version=UKOVersion(
version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
version="0.1.0",
label="Test",
comment="Test",
),
prefixes=(
UKOPrefix(
prefix="uko",
uri="https://cleveragents.ai/ontology/uko#",
),
),
nodes=(
UKONode(
uri="badprefix:Thing",
rdf_type="owl:Class",
label="Thing",
layer=0,
),
),
)
context.loader = UKOLoader()
@given("a UKO ontology with a cycle in inheritance")
def step_given_cycle(context: Context) -> None:
context.ontology = UKOOntology(
version=UKOVersion(
version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
version="0.1.0",
label="Test",
comment="Test",
),
prefixes=(
UKOPrefix(
prefix="uko",
uri="https://cleveragents.ai/ontology/uko#",
),
),
nodes=(
UKONode(
uri="https://cleveragents.ai/ontology/uko#A",
rdf_type="owl:Class",
label="A",
parent_uris=("https://cleveragents.ai/ontology/uko#B",),
layer=0,
),
UKONode(
uri="https://cleveragents.ai/ontology/uko#B",
rdf_type="owl:Class",
label="B",
parent_uris=("https://cleveragents.ai/ontology/uko#A",),
layer=0,
),
),
)
context.loader = UKOLoader()
@given("a UKO ontology with a node referencing a non-existent parent")
def step_given_nonexistent_parent(context: Context) -> None:
context.ontology = UKOOntology(
version=UKOVersion(
version_iri="https://cleveragents.ai/ontology/uko/0.1.0",
version="0.1.0",
label="Test",
comment="Test",
),
prefixes=(
UKOPrefix(
prefix="uko",
uri="https://cleveragents.ai/ontology/uko#",
),
),
nodes=(
UKONode(
uri="https://cleveragents.ai/ontology/uko#Orphan",
rdf_type="owl:Class",
label="Orphan",
parent_uris=("https://cleveragents.ai/ontology/uko#DoesNotExist",),
layer=0,
),
),
)
context.loader = UKOLoader()
# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------
@when("I parse the TTL string")
def step_when_parse_ttl(context: Context) -> None:
loader = UKOLoader()
context.ontology = loader.load_from_string(context.ttl_text)
context.loader = loader
@when("I get nodes at layer {layer:d}")
def step_when_get_layer(context: Context, layer: int) -> None:
loader: UKOLoader = getattr(context, "loader", UKOLoader())
context.layer_nodes = loader.get_layer_nodes(context.ontology, layer)
@when("I validate the ontology")
def step_when_validate(context: Context) -> None:
loader: UKOLoader = getattr(context, "loader", UKOLoader())
context.validation_errors = loader.validate(context.ontology)
@when('I resolve inheritance for "{uri}"')
def step_when_resolve_inheritance(context: Context, uri: str) -> None:
loader: UKOLoader = getattr(context, "loader", UKOLoader())
context.inheritance_chain = loader.resolve_inheritance(context.ontology, uri)
@when("I parse the TTL string expecting no nodes")
def step_when_parse_expecting_no_nodes(context: Context) -> None:
loader = UKOLoader()
# This TTL has a node without rdf:type — the parser skips it
context.ontology = loader.load_from_string(context.ttl_text)
context.loader = loader
@when("I inject a node without rdf_type into the ontology")
def step_inject_typeless_node(context: Context) -> None:
typeless = UKONode(
uri="https://cleveragents.ai/ontology/uko#Broken",
rdf_type="",
label="Broken",
)
existing = list(context.ontology.nodes)
existing.append(typeless)
context.ontology = context.ontology.model_copy(
update={"nodes": tuple(existing)},
)
@when("I validate the modified ontology")
def step_validate_modified(context: Context) -> None:
loader = UKOLoader()
context.validation_errors = loader.validate(context.ontology)
@when("I attempt to resolve inheritance for the cyclic node")
def step_when_resolve_cycle(context: Context) -> None:
loader: UKOLoader = context.loader
try:
loader.resolve_inheritance(
context.ontology,
"https://cleveragents.ai/ontology/uko#A",
)
context.cycle_error = None
except UKOValidationError as exc:
context.cycle_error = str(exc)
@when("I attempt to parse the TTL string expecting a validation error")
def step_when_parse_expecting_error(context: Context) -> None:
loader = UKOLoader()
try:
loader.load_from_string(context.ttl_text)
context.validation_error_msg = None
except UKOValidationError as exc:
context.validation_error_msg = str(exc)
# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------
@then('the prefix "{name}" should map to "{uri}"')
def step_then_prefix_maps(context: Context, name: str, uri: str) -> None:
prefix_map = {p.prefix: p.uri for p in context.ontology.prefixes}
assert name in prefix_map, f"Prefix '{name}' not found; have {list(prefix_map)}"
assert prefix_map[name] == uri, f"Expected {uri}, got {prefix_map[name]}"
@then('I should find a node with label "{label}"')
def step_then_find_node(context: Context, label: str) -> None:
labels = [n.label for n in context.layer_nodes]
assert label in labels, f"Label '{label}' not found in {labels}"
@then('the ontology version should be "{version}"')
def step_then_version(context: Context, version: str) -> None:
assert context.ontology.version.version == version
@then('the ontology label should be "{label}"')
def step_then_label(context: Context, label: str) -> None:
assert context.ontology.version.label == label
@then('the ontology comment should be "{comment}"')
def step_then_comment(context: Context, comment: str) -> None:
assert context.ontology.version.comment == comment
@then('the version IRI should be "{iri}"')
def step_then_version_iri(context: Context, iri: str) -> None:
assert context.ontology.version.version_iri == iri
@then("no nodes should be present in the ontology")
def step_then_no_nodes(context: Context) -> None:
# The "Broken" node has no rdf:type, so the parser skips it
broken_nodes = [n for n in context.ontology.nodes if n.label == "Broken"]
assert len(broken_nodes) == 0, f"Expected no 'Broken' nodes, found {broken_nodes}"
@then('the validation errors should mention "{text}"')
def step_then_validation_mentions(context: Context, text: str) -> None:
combined = " ".join(context.validation_errors)
assert text in combined, f"Expected '{text}' in errors: {context.validation_errors}"
@then("there should be no validation errors")
def step_then_no_errors(context: Context) -> None:
assert context.validation_errors == [], (
f"Expected no errors, got: {context.validation_errors}"
)
@then("the inheritance chain should have at least {count:d} nodes")
def step_then_chain_length(context: Context, count: int) -> None:
assert len(context.inheritance_chain) >= count, (
f"Expected >= {count} nodes, got {len(context.inheritance_chain)}"
)
@then('the first node in the chain should have label "{label}"')
def step_then_first_chain_label(context: Context, label: str) -> None:
assert context.inheritance_chain[0].label == label, (
f"Expected '{label}', got '{context.inheritance_chain[0].label}'"
)
@then('the last node in the chain should have label "{label}"')
def step_then_last_chain_label(context: Context, label: str) -> None:
assert context.inheritance_chain[-1].label == label, (
f"Expected '{label}', got '{context.inheritance_chain[-1].label}'"
)
@then("there should be at least {count:d} nodes")
def step_then_min_nodes(context: Context, count: int) -> None:
assert len(context.layer_nodes) >= count, (
f"Expected >= {count}, got {len(context.layer_nodes)}"
)
@then("the ontology should have prefixes defined")
def step_then_has_prefixes(context: Context) -> None:
assert len(context.ontology.prefixes) > 0
@then("the ontology should have nodes defined")
def step_then_has_nodes(context: Context) -> None:
assert len(context.ontology.nodes) > 0
@then('"{version}" should be in the supported versions')
def step_then_supported_version(context: Context, version: str) -> None:
assert version in context.ontology.supported_versions
@then("the node count should be {count:d}")
def step_node_count(context: Context, count: int) -> None:
assert len(context.layer_nodes) == count, (
f"Expected {count} nodes but got {len(context.layer_nodes)}"
)
@then('validation errors should include "{text}"')
def step_validation_errors_include(context: Context, text: str) -> None:
assert any(text in e for e in context.validation_errors), (
f"Expected '{text}' in validation errors: {context.validation_errors}"
)
@then('the node with label "{label}" should have parent "{parent_uri}"')
def step_then_node_has_parent(
context: Context,
label: str,
parent_uri: str,
) -> None:
matching = [n for n in context.layer_nodes if n.label == label]
assert matching, f"No node with label '{label}' found in layer nodes"
node = matching[0]
assert parent_uri in node.parent_uris, (
f"Expected parent '{parent_uri}' for node '{label}'; got: {node.parent_uris}"
)
@then(
'the node with label "{label}" should have property "{prop}" resolved to a full URI'
)
def step_then_property_resolved(context: Context, label: str, prop: str) -> None:
matching = [n for n in context.layer_nodes if n.label == label]
assert matching, f"No node with label '{label}' found in layer nodes"
node = matching[0]
prop_dict = dict(node.properties)
assert prop in prop_dict, (
f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}"
)
value = prop_dict[prop]
assert value.startswith("http://") or value.startswith("https://"), (
f"Expected full URI for '{prop}' on '{label}', got: '{value}'"
)
@then(
'the node with label "{label}" should have property "{prop}" equal to "{expected}"'
)
def step_then_property_equal(
context: Context,
label: str,
prop: str,
expected: str,
) -> None:
matching = [n for n in context.layer_nodes if n.label == label]
assert matching, f"No node with label '{label}' found in layer nodes"
node = matching[0]
prop_dict = dict(node.properties)
assert prop in prop_dict, (
f"Property '{prop}' not found on node '{label}'; have: {list(prop_dict)}"
)
assert prop_dict[prop] == expected, (
f"Expected '{prop}' == '{expected}' on '{label}', got: '{prop_dict[prop]}'"
)
@then('the node with label "{label}" should not have property "{prop}"')
def step_then_property_absent(context: Context, label: str, prop: str) -> None:
matching = [n for n in context.layer_nodes if n.label == label]
assert matching, f"No node with label '{label}' found in layer nodes"
node = matching[0]
prop_dict = dict(node.properties)
assert prop not in prop_dict, (
f"Expected no '{prop}' on '{label}', but found: '{prop_dict[prop]}'"
)
@then("a cycle detection error should be raised")
def step_then_cycle_error(context: Context) -> None:
assert context.cycle_error is not None, "Expected cycle error but none raised"
assert "Cycle" in context.cycle_error or "cycle" in context.cycle_error
@then('a validation error should mention "{text}"')
def step_then_validation_error_mention(context: Context, text: str) -> None:
assert context.validation_error_msg is not None, (
"Expected a UKOValidationError but none was raised"
)
assert text in context.validation_error_msg, (
f"Expected '{text}' in error message: {context.validation_error_msg}"
)
# ---------------------------------------------------------------------------
# Duplicate-key handling in properties tuple
# ---------------------------------------------------------------------------
@given("a UKO node with duplicate property keys")
def step_given_duplicate_props(context: Context) -> None:
context.dup_node = UKONode(
uri="https://cleveragents.ai/ontology/uko#TestDup",
rdf_type="owl:DatatypeProperty",
label="TestDup",
properties=(
("rdfs:domain", "https://cleveragents.ai/ontology/uko#First"),
("rdfs:domain", "https://cleveragents.ai/ontology/uko#Second"),
),
)
@when("I convert the node properties to a dict")
def step_when_convert_props_dict(context: Context) -> None:
context.prop_dict = dict(context.dup_node.properties)
@then("the dict should contain only the last value for the duplicated key")
def step_then_last_value(context: Context) -> None:
assert context.prop_dict["rdfs:domain"] == (
"https://cleveragents.ai/ontology/uko#Second"
), f"Expected last value 'Second', got: {context.prop_dict['rdfs:domain']}"
+352
View File
@@ -0,0 +1,352 @@
@phase2 @uko @ontology
Feature: UKO Ontology Loader
As a CleverAgents developer
I want to parse and validate the UKO ontology from TTL files
So that the system can reason about code artifacts and their relationships
# ---------------------------------------------------------------------------
# Prefix parsing
# ---------------------------------------------------------------------------
@uko_prefix
Scenario: Parse prefix line correctly
Given a TTL string:
"""
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
@prefix uko-code: <https://cleveragents.ai/ontology/uko/code#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
uko: a owl:Ontology ;
owl:versionIRI <https://cleveragents.ai/ontology/uko/0.1.0> ;
rdfs:label "Test" ;
rdfs:comment "Test ontology" .
"""
When I parse the TTL string
Then the prefix "uko" should map to "https://cleveragents.ai/ontology/uko#"
And the prefix "uko-code" should map to "https://cleveragents.ai/ontology/uko/code#"
And the prefix "rdf" should map to "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
# ---------------------------------------------------------------------------
# Layer 0 — Universal Foundation
# ---------------------------------------------------------------------------
@uko_layer0
Scenario: Parse Layer 0 nodes from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then I should find a node with label "InformationUnit"
And I should find a node with label "Container"
And I should find a node with label "Atom"
And I should find a node with label "Annotation"
And I should find a node with label "Boundary"
# ---------------------------------------------------------------------------
# Layer 1 — General Software
# ---------------------------------------------------------------------------
@uko_layer1
Scenario: Parse Layer 1 nodes from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 1
Then I should find a node with label "Module"
And I should find a node with label "Callable"
And I should find a node with label "TypeDefinition"
And I should find a node with label "TestCase"
And I should find a node with label "Import"
# ---------------------------------------------------------------------------
# Layer 2 — Object-Oriented Paradigm
# ---------------------------------------------------------------------------
@uko_layer2
Scenario: Parse Layer 2 nodes from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 2
Then I should find a node with label "Class"
And I should find a node with label "Interface"
And I should find a node with label "Method"
And I should find a node with label "Attribute"
# ---------------------------------------------------------------------------
# Layer 0 properties (relationships)
# ---------------------------------------------------------------------------
@uko_layer0_props
Scenario: Parse Layer 0 relationship properties from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then I should find a node with label "contains"
And I should find a node with label "references"
And I should find a node with label "dependsOn"
# ---------------------------------------------------------------------------
# Version metadata
# ---------------------------------------------------------------------------
@uko_version
Scenario: Version metadata extracted correctly
Given the UKO ontology is loaded from the TTL file
Then the ontology version should be "0.1.0"
And the ontology label should be "Unified Knowledge Ontology"
And the ontology comment should be "CleverAgents knowledge representation layers 0-3"
And the version IRI should be "https://cleveragents.ai/ontology/uko/0.1.0"
# ---------------------------------------------------------------------------
# Validation errors
# ---------------------------------------------------------------------------
@uko_validation @error_handling
Scenario: Parser skips nodes without rdf_type
Given a TTL string with a node missing rdf_type:
"""
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
uko: a owl:Ontology ;
owl:versionIRI <https://cleveragents.ai/ontology/uko/0.1.0> ;
rdfs:label "Test" ;
rdfs:comment "Test" .
uko:Broken rdfs:label "Broken" .
"""
When I parse the TTL string expecting no nodes
Then no nodes should be present in the ontology
@uko_validation @error_handling
Scenario: Validate rejects ontology with typeless node
Given the UKO ontology is loaded from the TTL file
When I inject a node without rdf_type into the ontology
And I validate the modified ontology
Then validation errors should include "missing rdf:type"
@uko_validation @error_handling
Scenario: Validate rejects undefined prefix
Given a UKO ontology with an undefined prefix node
When I validate the ontology
Then the validation errors should mention "undefined prefix"
@uko_validation
Scenario: Validate accepts valid ontology
Given the UKO ontology is loaded from the TTL file
When I validate the ontology
Then there should be no validation errors
@uko_validation @error_handling
Scenario: Validate rejects non-existent parent URI
Given a UKO ontology with a node referencing a non-existent parent
When I validate the ontology
Then the validation errors should mention "non-existent parent"
# ---------------------------------------------------------------------------
# Inheritance resolution
# ---------------------------------------------------------------------------
@uko_inheritance
Scenario: Resolve inheritance DAG for uko-oo:Class (multi-parent)
Given the UKO ontology is loaded from the TTL file
When I resolve inheritance for "https://cleveragents.ai/ontology/uko/oo#Class"
Then the inheritance chain should have at least 5 nodes
And the first node in the chain should have label "Class"
And the last node in the chain should have label "InformationUnit"
@uko_inheritance
Scenario: Resolve inheritance chain for Container (Layer 0)
Given the UKO ontology is loaded from the TTL file
When I resolve inheritance for "https://cleveragents.ai/ontology/uko#Container"
Then the inheritance chain should have at least 2 nodes
And the first node in the chain should have label "Container"
@uko_inheritance @multi_parent
Scenario: uko-oo:Class has dual rdfs:subClassOf parents
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 2
Then the node with label "Class" should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition"
And the node with label "Class" should have parent "https://cleveragents.ai/ontology/uko#Container"
@uko_inheritance @multi_parent
Scenario: uko-oo:Interface has dual rdfs:subClassOf parents
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 2
Then the node with label "Interface" should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition"
And the node with label "Interface" should have parent "https://cleveragents.ai/ontology/uko#Boundary"
# ---------------------------------------------------------------------------
# Layer query
# ---------------------------------------------------------------------------
@uko_layer_query
Scenario: Get all nodes at specific layer
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then there should be at least 18 nodes
@uko_layer
Scenario Outline: Get correct node count for layer <layer>
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer <layer>
Then the node count should be <count>
Examples:
| layer | count |
| 0 | 18 |
| 1 | 8 |
| 2 | 6 |
# ---------------------------------------------------------------------------
# rdfs:domain and rdfs:range URI resolution
# ---------------------------------------------------------------------------
@uko_domain_range
Scenario: rdfs:domain values are resolved to full URIs
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then the node with label "contains" should have property "rdfs:domain" resolved to a full URI
And the node with label "contains" should have property "rdfs:range" resolved to a full URI
@uko_domain_range
Scenario: rdfs:subPropertyOf values are resolved to full URIs
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 2
Then the node with label "inheritsFrom" should have property "rdfs:subPropertyOf" resolved to a full URI
# ---------------------------------------------------------------------------
# Integration: load actual file
# ---------------------------------------------------------------------------
@uko_integration
Scenario: Load the actual uko.ttl file successfully
Given the UKO ontology is loaded from the TTL file
Then the ontology should have prefixes defined
And the ontology should have nodes defined
# ---------------------------------------------------------------------------
# Version registry
# ---------------------------------------------------------------------------
@uko_version
Scenario: Version registry contains 0.1.0
Given the UKO ontology is loaded from the TTL file
Then "0.1.0" should be in the supported versions
# ---------------------------------------------------------------------------
# Layer 0 content, provenance, and temporal properties
# ---------------------------------------------------------------------------
@uko_layer0_props
Scenario: Parse Layer 0 content properties from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then I should find a node with label "hasRendering"
And I should find a node with label "renderingDepth"
And I should find a node with label "hasFullContent"
@uko_layer0_props
Scenario: Parse Layer 0 provenance properties from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then I should find a node with label "sourceResource"
And I should find a node with label "sourcePath"
And I should find a node with label "sourceRange"
@uko_layer0_props
Scenario: Parse Layer 0 temporal properties from TTL
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then I should find a node with label "validFrom"
And I should find a node with label "validUntil"
And I should find a node with label "isCurrent"
And I should find a node with label "isRevisionOf"
# ---------------------------------------------------------------------------
# rdfs:domain / rdfs:range correctness for content, provenance, temporal props
# ---------------------------------------------------------------------------
@uko_domain_range
Scenario: Content properties have correct rdfs:domain and rdfs:range
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then the node with label "hasRendering" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "hasRendering" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#string"
And the node with label "renderingDepth" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "renderingDepth" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#nonNegativeInteger"
And the node with label "hasFullContent" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "hasFullContent" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#string"
@uko_domain_range
Scenario: Provenance properties have correct rdfs:domain and rdfs:range
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then the node with label "sourceResource" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "sourceResource" should not have property "rdfs:range"
And the node with label "sourcePath" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "sourcePath" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#string"
And the node with label "sourceRange" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "sourceRange" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#string"
@uko_domain_range
Scenario: Temporal properties have correct rdfs:domain and rdfs:range
Given the UKO ontology is loaded from the TTL file
When I get nodes at layer 0
Then the node with label "validFrom" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "validFrom" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#dateTime"
And the node with label "validUntil" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "validUntil" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#dateTime"
And the node with label "isCurrent" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "isCurrent" should have property "rdfs:range" equal to "http://www.w3.org/2001/XMLSchema#boolean"
And the node with label "isRevisionOf" should have property "rdfs:domain" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
And the node with label "isRevisionOf" should have property "rdfs:range" equal to "https://cleveragents.ai/ontology/uko#InformationUnit"
# ---------------------------------------------------------------------------
# Unsupported version rejection
# ---------------------------------------------------------------------------
@uko_validation @error_handling
Scenario: Loader rejects unsupported ontology version
Given a TTL string:
"""
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
uko: a owl:Ontology ;
owl:versionIRI <https://cleveragents.ai/ontology/uko/99.0.0> ;
rdfs:label "Test" ;
rdfs:comment "Test" .
"""
When I attempt to parse the TTL string expecting a validation error
Then a validation error should mention "Unsupported version"
@uko_validation @error_handling
Scenario: Loader rejects version IRI with non-semver last segment
Given a TTL string:
"""
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
uko: a owl:Ontology ;
owl:versionIRI <https://cleveragents.ai/ontology/uko> ;
rdfs:label "Test" ;
rdfs:comment "Test" .
"""
When I attempt to parse the TTL string expecting a validation error
Then a validation error should mention "Unsupported version"
# ---------------------------------------------------------------------------
# Duplicate-key handling in properties tuple
# ---------------------------------------------------------------------------
@uko_properties
Scenario: Duplicate keys in properties tuple preserve last value when cast to dict
Given a UKO node with duplicate property keys
When I convert the node properties to a dict
Then the dict should contain only the last value for the duplicated key
# ---------------------------------------------------------------------------
# Cycle detection
# ---------------------------------------------------------------------------
@uko_inheritance @error_handling
Scenario: Cycle detection in inheritance
Given a UKO ontology with a cycle in inheritance
When I attempt to resolve inheritance for the cyclic node
Then a cycle detection error should be raised
+132
View File
@@ -0,0 +1,132 @@
"""Robot Framework helper for UKO ontology integration tests.
Subcommands:
load-ttl -- load docs/ontology/uko.ttl, print ``uko-load-ok``
validate -- load and validate, print ``uko-validate-ok``
resolve-inheritance -- resolve uko-oo:Class chain, print ``uko-inheritance-ok``
layer-nodes -- get layer 0 nodes, print ``uko-layer-nodes-ok``
"""
from __future__ import annotations
import sys
from collections.abc import Callable
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.application.services.uko_loader import UKOLoader # noqa: E402
_TTL_PATH = Path(__file__).resolve().parents[1] / "docs" / "ontology" / "uko.ttl"
def _cmd_load_ttl() -> int:
"""Load the TTL file and verify basic parsing."""
try:
loader = UKOLoader()
ontology = loader.load(_TTL_PATH)
except Exception as exc:
print(f"uko-fail: {exc}")
return 1
if not ontology.nodes:
print("uko-fail: no nodes parsed")
return 1
print(
f"uko-load-ok: {len(ontology.nodes)} nodes, {len(ontology.prefixes)} prefixes"
)
return 0
def _cmd_validate() -> int:
"""Load and validate the ontology."""
try:
loader = UKOLoader()
ontology = loader.load(_TTL_PATH)
errors = loader.validate(ontology)
except Exception as exc:
print(f"uko-fail: {exc}")
return 1
if errors:
print(f"uko-fail: validation errors: {errors}")
return 1
print(f"uko-validate-ok: version={ontology.version.version}")
return 0
def _cmd_resolve_inheritance() -> int:
"""Resolve uko-oo:Class inheritance chain (Layer 2 -> 1 -> 0)."""
try:
loader = UKOLoader()
ontology = loader.load(_TTL_PATH)
chain = loader.resolve_inheritance(
ontology,
"https://cleveragents.ai/ontology/uko/oo#Class",
)
except Exception as exc:
print(f"uko-fail: {exc}")
return 1
if len(chain) < 3:
print(f"uko-fail: chain too short ({len(chain)} nodes)")
return 1
print(f"uko-inheritance-ok: {len(chain)} nodes")
for node in chain:
print(f" layer={node.layer} label={node.label}")
return 0
def _cmd_layer_nodes() -> int:
"""Get all Layer 0 nodes."""
try:
loader = UKOLoader()
ontology = loader.load(_TTL_PATH)
nodes = loader.get_layer_nodes(ontology, 0)
except Exception as exc:
print(f"uko-fail: {exc}")
return 1
if len(nodes) < 18:
print(f"uko-fail: expected >= 18 layer-0 nodes, got {len(nodes)}")
return 1
print(f"uko-layer-nodes-ok: {len(nodes)} nodes")
for node in nodes:
print(f" {node.label} ({node.uri})")
return 0
_COMMANDS: dict[str, Callable[[], int]] = {
"load-ttl": _cmd_load_ttl,
"validate": _cmd_validate,
"resolve-inheritance": _cmd_resolve_inheritance,
"layer-nodes": _cmd_layer_nodes,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print(
"Usage: helper_uko_ontology.py "
"<load-ttl|validate|resolve-inheritance|layer-nodes>"
)
return 1
command = sys.argv[1]
handler = _COMMANDS.get(command)
if handler is None:
print(f"Unknown command: {command}")
return 1
return handler()
if __name__ == "__main__":
sys.exit(main())
+41
View File
@@ -0,0 +1,41 @@
*** Settings ***
Documentation Integration smoke tests for UKO ontology loader
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_uko_ontology.py
*** Test Cases ***
Load UKO Ontology From TTL File
[Documentation] Load docs/ontology/uko.ttl and verify parsing succeeds
${result}= Run Process ${PYTHON} ${HELPER} load-ttl cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} uko-load-ok
Validate UKO Ontology
[Documentation] Load and validate the ontology with no errors
${result}= Run Process ${PYTHON} ${HELPER} validate cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} uko-validate-ok
Resolve Inheritance Chain
[Documentation] Resolve uko-oo:Class inheritance chain through layers
${result}= Run Process ${PYTHON} ${HELPER} resolve-inheritance cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} uko-inheritance-ok
Get Layer Nodes
[Documentation] Retrieve all Layer 0 foundation nodes
${result}= Run Process ${PYTHON} ${HELPER} layer-nodes cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} uko-layer-nodes-ok
@@ -125,6 +125,10 @@ from cleveragents.application.services.tool_registry_service import (
from cleveragents.application.services.trace_service import (
TraceService,
)
from cleveragents.application.services.uko_loader import (
UKOLoader,
UKOValidationError,
)
from cleveragents.application.services.validation_apply import (
ApplyValidationGate,
ApplyValidationResult,
@@ -213,6 +217,8 @@ __all__ = [
"SyntaxCheckRule",
"ToolRegistryService",
"TraceService",
"UKOLoader",
"UKOValidationError",
"ValidationAttachment",
"ValidationCommand",
"ValidationPipeline",
@@ -0,0 +1,536 @@
"""UKO ontology loader — lightweight TTL parser with no external RDF deps.
Parses ``@prefix``, node declarations (``subject a Type ;``),
``rdfs:subClassOf``, ``rdfs:label``, ``rdfs:comment``, ``rdfs:domain``,
``rdfs:range``, and ``owl:versionIRI`` from Turtle files into
:class:`UKOOntology` Pydantic models.
"""
from __future__ import annotations
import re
from collections import deque
from pathlib import Path
from typing import ClassVar
from cleveragents.domain.models.core.uko import (
UKONode,
UKOOntology,
UKOPrefix,
UKOVersion,
)
# ---------------------------------------------------------------------------
# Exceptions
# ---------------------------------------------------------------------------
class UKOValidationError(Exception):
"""Raised when ontology validation fails."""
# ---------------------------------------------------------------------------
# Regex helpers
# ---------------------------------------------------------------------------
_PREFIX_RE = re.compile(
r"^@prefix\s+([\w-]*)\s*:\s*<([^>]+)>\s*\.\s*$",
)
_IRI_RE = re.compile(r"<([^>]+)>")
_STRING_RE = re.compile(r'"([^"]*)"')
# Layer prefix patterns — maps prefixed-name prefixes to layer numbers.
# Uses the spec-defined semantic domain prefixes (not numeric indices).
_LAYER_PREFIXES: dict[str, int] = {
"uko:": 0,
"uko-code:": 1,
"uko-doc:": 1,
"uko-data:": 1,
"uko-infra:": 1,
"uko-oo:": 2,
"uko-func:": 2,
"uko-proc:": 2,
"uko-py:": 3,
"uko-ts:": 3,
"uko-rs:": 3,
"uko-java:": 3,
}
# Full-URI layer detection — maps IRI namespace prefixes to layers.
_LAYER_IRI_PREFIXES: dict[str, int] = {
"https://cleveragents.ai/ontology/uko#": 0,
"https://cleveragents.ai/ontology/uko/code#": 1,
"https://cleveragents.ai/ontology/uko/doc#": 1,
"https://cleveragents.ai/ontology/uko/data#": 1,
"https://cleveragents.ai/ontology/uko/infra#": 1,
"https://cleveragents.ai/ontology/uko/oo#": 2,
"https://cleveragents.ai/ontology/uko/func#": 2,
"https://cleveragents.ai/ontology/uko/proc#": 2,
"https://cleveragents.ai/ontology/uko/py#": 3,
"https://cleveragents.ai/ontology/uko/ts#": 3,
"https://cleveragents.ai/ontology/uko/rs#": 3,
"https://cleveragents.ai/ontology/uko/java#": 3,
}
def _detect_layer(uri: str) -> int:
"""Determine which layer (0-3) a URI belongs to based on prefix.
Checks both prefixed names (``uko-code:Module``) and full IRIs
(``https://cleveragents.ai/ontology/uko/code#Module``).
"""
# Check prefixed names first (more common during parsing)
for prefix, layer in _LAYER_PREFIXES.items():
if uri.startswith(prefix):
return layer
# Check full IRIs
for iri_prefix, layer in _LAYER_IRI_PREFIXES.items():
if uri.startswith(iri_prefix):
return layer
return 0
def _extract_string(value: str) -> str:
"""Extract a quoted string value from a TTL predicate object."""
match = _STRING_RE.search(value)
return match.group(1) if match else value.strip().strip('"')
def _extract_iri(value: str) -> str:
"""Extract an IRI (<...>) from a TTL predicate object."""
match = _IRI_RE.search(value)
return match.group(1) if match else value.strip()
# ---------------------------------------------------------------------------
# Loader
# ---------------------------------------------------------------------------
class UKOLoader:
"""Load and validate UKO ontology from TTL files."""
SUPPORTED_VERSIONS: ClassVar[tuple[str, ...]] = ("0.1.0",)
def load(self, ttl_path: Path) -> UKOOntology:
"""Parse a TTL file into a :class:`UKOOntology` model."""
text = ttl_path.read_text(encoding="utf-8")
return self.load_from_string(text)
def load_from_string(self, text: str) -> UKOOntology:
"""Parse a TTL string into a :class:`UKOOntology` model."""
prefixes = self._parse_prefixes(text)
prefix_map = {p.prefix: p.uri for p in prefixes}
blocks = self._split_blocks(text)
nodes: list[UKONode] = []
version_iri = ""
version_label = ""
version_comment = ""
for subject, predicates in blocks:
props = self._parse_predicates(predicates)
# Ontology declaration (subject is bare prefix like "uko:")
if (
props.get("a") == "owl:Ontology"
or props.get("rdf:type") == "owl:Ontology"
):
version_iri = props.get("owl:versionIRI", "")
version_label = _extract_string(props.get("rdfs:label", ""))
version_comment = _extract_string(props.get("rdfs:comment", ""))
continue
rdf_type = props.get("a", props.get("rdf:type", ""))
if not rdf_type:
continue
raw_parents = props.get("rdfs:subClassOf")
parent_uris: tuple[str, ...] = ()
if raw_parents:
# Handle comma-separated multi-parent values
# e.g. "uko-code:TypeDefinition , uko:Container"
parent_uris = tuple(
self._resolve_uri(p.strip(), prefix_map)
for p in raw_parents.split(",")
if p.strip()
)
label = _extract_string(props.get("rdfs:label", ""))
comment = _extract_string(props.get("rdfs:comment", ""))
extra: dict[str, str] = {}
if "rdfs:domain" in props:
extra["rdfs:domain"] = props["rdfs:domain"]
if "rdfs:range" in props:
extra["rdfs:range"] = props["rdfs:range"]
if "rdfs:subPropertyOf" in props:
extra["rdfs:subPropertyOf"] = props["rdfs:subPropertyOf"]
# Resolve any prefixed property values to full URIs
_resolvable_props = ("rdfs:domain", "rdfs:range", "rdfs:subPropertyOf")
resolved_props: list[tuple[str, str]] = []
for prop_key, prop_val in extra.items():
if (
prop_key in _resolvable_props
and ":" in prop_val
and not prop_val.startswith("http")
):
resolved_props.append(
(prop_key, self._resolve_uri(prop_val, prefix_map)),
)
else:
resolved_props.append((prop_key, prop_val))
uri = self._resolve_uri(subject, prefix_map)
layer = _detect_layer(subject)
nodes.append(
UKONode(
uri=uri,
rdf_type=rdf_type,
label=label,
comment=comment,
parent_uris=parent_uris,
layer=layer,
properties=tuple(resolved_props),
),
)
version_str = self._extract_version_string(version_iri)
ontology = UKOOntology(
version=UKOVersion(
version_iri=version_iri,
version=version_str,
label=version_label,
comment=version_comment,
),
prefixes=tuple(prefixes),
nodes=tuple(nodes),
)
errors = self.validate(ontology)
if errors:
raise UKOValidationError("; ".join(errors))
return ontology
# ------------------------------------------------------------------
# Validation
# ------------------------------------------------------------------
def validate(self, ontology: UKOOntology) -> list[str]:
"""Validate ontology and return list of errors (empty = valid)."""
errors: list[str] = []
known_prefixes = {p.prefix for p in ontology.prefixes}
node_uris = {n.uri for n in ontology.nodes}
_uko_ns = "https://cleveragents.ai/ontology/uko"
for node in ontology.nodes:
if not node.rdf_type:
errors.append(f"Node '{node.uri}' missing rdf:type")
for resolved_parent in node.parent_uris:
if resolved_parent.startswith("http://") or resolved_parent.startswith(
"https://"
):
# Internal UKO URIs must resolve to a known node.
# Guard with delimiter to avoid false matches on URIs
# that merely share the prefix string (e.g. "ukobogus").
is_internal = resolved_parent.startswith(
_uko_ns + "#"
) or resolved_parent.startswith(_uko_ns + "/")
if is_internal and resolved_parent not in node_uris:
errors.append(
f"Node '{node.uri}' references "
f"non-existent parent '{resolved_parent}'"
)
# Truly external URIs (w3.org, etc.) are valid
elif ":" in resolved_parent:
prefix_part = resolved_parent.split(":")[0]
if prefix_part not in known_prefixes:
errors.append(
f"Node '{node.uri}' references undefined "
f"prefix '{prefix_part}:' in parent_uri"
)
elif resolved_parent not in node_uris:
errors.append(
f"Node '{node.uri}' references "
f"non-existent parent '{resolved_parent}'"
)
# Check for undefined prefixes in URI
if ":" in node.uri and not node.uri.startswith("http"):
prefix_part = node.uri.split(":")[0]
if prefix_part not in known_prefixes:
errors.append(
f"Node '{node.uri}' uses undefined prefix '{prefix_part}:'"
)
if ontology.version.version not in self.SUPPORTED_VERSIONS:
errors.append(
f"Unsupported version '{ontology.version.version}'; "
f"supported: {', '.join(self.SUPPORTED_VERSIONS)}"
)
return errors
# ------------------------------------------------------------------
# Query helpers
# ------------------------------------------------------------------
def resolve_inheritance(
self,
ontology: UKOOntology,
node_uri: str,
) -> list[UKONode]:
"""Resolve the inheritance DAG from *node_uri* up to root(s).
Uses BFS (``collections.deque``) to traverse all
``rdfs:subClassOf`` parents, producing a list ordered leaf-first.
Diamond inheritance is handled correctly each ancestor appears
at most once. Raises :class:`UKOValidationError` on cycles.
"""
uri_map: dict[str, UKONode] = {n.uri: n for n in ontology.nodes}
# Phase 1: cycle detection via DFS with gray/black colouring
self._detect_cycle_dfs(uri_map, node_uri)
# Phase 2: BFS traversal for ordered ancestor list
chain: list[UKONode] = []
seen: set[str] = set()
queue: deque[str] = deque()
queue.append(node_uri)
seen.add(node_uri)
while queue:
current_uri = queue.popleft()
node = uri_map.get(current_uri)
if node is None:
continue
chain.append(node)
for parent in node.parent_uris:
if parent not in seen:
seen.add(parent)
queue.append(parent)
return chain
def get_layer_nodes(
self,
ontology: UKOOntology,
layer: int,
) -> list[UKONode]:
"""Return all nodes at a specific layer.
.. note::
This is O(n) per call. For projects with 10,000+ indexed files
(milestone v3.4.0), consider precomputing a ``dict[int,
list[UKONode]]`` index during load.
"""
# TODO(perf): precompute layer index when scaling demands it.
return [n for n in ontology.nodes if n.layer == layer]
# ------------------------------------------------------------------
# Cycle detection
# ------------------------------------------------------------------
@staticmethod
def _detect_cycle_dfs(
uri_map: dict[str, UKONode],
start_uri: str,
) -> None:
"""Raise :class:`UKOValidationError` if a cycle exists in the DAG.
Uses iterative DFS with gray/black colouring to detect back-edges
in the ``rdfs:subClassOf`` hierarchy reachable from *start_uri*.
"""
# 0 = white (unvisited), 1 = gray (in progress), 2 = black (done)
colour: dict[str, int] = {}
stack: list[tuple[str, bool]] = [(start_uri, False)]
while stack:
uri, returning = stack.pop()
if returning:
colour[uri] = 2 # black — fully explored
continue
state = colour.get(uri, 0)
if state == 2:
continue # already fully explored
if state == 1:
raise UKOValidationError(f"Cycle detected in inheritance chain: {uri}")
colour[uri] = 1 # gray — in progress
stack.append((uri, True)) # return marker
node = uri_map.get(uri)
if node is None:
continue
for parent in node.parent_uris:
parent_state = colour.get(parent, 0)
if parent_state == 1:
raise UKOValidationError(
f"Cycle detected in inheritance chain: {parent}"
)
if parent_state == 0:
stack.append((parent, False))
# ------------------------------------------------------------------
# Internal parsing helpers
# ------------------------------------------------------------------
def _parse_prefixes(self, text: str) -> list[UKOPrefix]:
"""Parse ``@prefix`` lines from TTL text."""
prefixes: list[UKOPrefix] = []
for line in text.splitlines():
match = _PREFIX_RE.match(line.strip())
if match:
prefixes.append(
UKOPrefix(prefix=match.group(1), uri=match.group(2)),
)
return prefixes
def _split_blocks(self, text: str) -> list[tuple[str, str]]:
"""Split TTL text into (subject, predicates) blocks.
Each block is a statement terminated by ``.`` (outside ``<>``, ``""``).
Prefix declarations and comment-only lines are skipped.
"""
lines: list[str] = []
for raw_line in text.splitlines():
stripped = raw_line.strip()
if (
not stripped
or stripped.startswith("#")
or stripped.startswith("@prefix")
):
continue
lines.append(stripped)
joined = " ".join(lines)
# Manually split on '.' that is a statement terminator, not inside
# angle brackets or quotes.
statements: list[str] = []
buf: list[str] = []
in_iri = False
in_str = False
i = 0
chars = joined
length = len(chars)
while i < length:
ch = chars[i]
if ch == "<" and not in_str:
in_iri = True
buf.append(ch)
elif ch == ">" and in_iri and not in_str:
in_iri = False
buf.append(ch)
elif ch == '"' and not in_iri:
in_str = not in_str
buf.append(ch)
elif ch == "." and not in_iri and not in_str:
# Check it's a statement terminator (followed by whitespace or end)
next_i = i + 1
if next_i >= length or chars[next_i] in (" ", "\t", "\n", "\r"):
stmt = "".join(buf).strip()
if stmt:
statements.append(stmt)
buf = []
else:
buf.append(ch)
else:
buf.append(ch)
i += 1
# Flush remaining buffer
remainder = "".join(buf).strip()
if remainder:
statements.append(remainder)
blocks: list[tuple[str, str]] = []
for stmt in statements:
parts = stmt.split(None, 1)
if len(parts) < 2:
continue
blocks.append((parts[0], parts[1]))
return blocks
def _parse_predicates(self, predicates_str: str) -> dict[str, str]:
"""Parse ``predicate object ;`` pairs into a dict."""
result: dict[str, str] = {}
# Split on `;` that is outside `<>` and `""`
pairs = self._split_on_semicolon(predicates_str)
for pair in pairs:
pair = pair.strip()
if not pair:
continue
parts = pair.split(None, 1)
if len(parts) < 2:
continue
predicate, obj = parts[0], parts[1]
# Handle IRI values
iri_match = _IRI_RE.search(obj)
if iri_match and predicate in (
"owl:versionIRI",
"rdfs:domain",
"rdfs:range",
"rdfs:subPropertyOf",
):
result[predicate] = iri_match.group(1)
else:
result[predicate] = obj.strip()
return result
@staticmethod
def _split_on_semicolon(text: str) -> list[str]:
"""Split *text* on ``;`` that is not inside ``<>`` or ``""``."""
parts: list[str] = []
buf: list[str] = []
in_iri = False
in_str = False
for ch in text:
if ch == "<" and not in_str:
in_iri = True
buf.append(ch)
elif ch == ">" and in_iri and not in_str:
in_iri = False
buf.append(ch)
elif ch == '"' and not in_iri:
in_str = not in_str
buf.append(ch)
elif ch == ";" and not in_iri and not in_str:
parts.append("".join(buf))
buf = []
else:
buf.append(ch)
remainder = "".join(buf).strip()
if remainder:
parts.append(remainder)
return parts
def _resolve_uri(self, prefixed: str, prefix_map: dict[str, str]) -> str:
"""Expand a prefixed name to a full URI if the prefix is known."""
if prefixed.startswith("<") and prefixed.endswith(">"):
return prefixed[1:-1]
if ":" in prefixed:
prefix, local = prefixed.split(":", 1)
if prefix in prefix_map:
return prefix_map[prefix] + local
return prefixed
def _extract_version_string(self, version_iri: str) -> str:
"""Extract a semver-like version from a version IRI.
E.g. ``https://ontology.cleverthis.com/uko/0.1.0`` -> ``0.1.0``.
Returns ``""`` if the last path segment is not a valid semver string.
"""
if not version_iri:
return ""
# Take the last path segment
segment = version_iri.rstrip("/").rsplit("/", 1)[-1]
if re.match(r"^\d+\.\d+\.\d+$", segment):
return segment
return ""
@@ -264,6 +264,14 @@ from cleveragents.domain.models.core.tool import (
ValidationMode,
)
# UKO ontology models
from cleveragents.domain.models.core.uko import (
UKONode,
UKOOntology,
UKOPrefix,
UKOVersion,
)
__all__ = [
"BUILTIN_PROFILES",
"DEFAULT_LOCAL_ROLE_MAPPING",
@@ -426,6 +434,10 @@ __all__ = [
"ToolLifecycle",
"ToolSource",
"ToolType",
"UKONode",
"UKOOntology",
"UKOPrefix",
"UKOVersion",
"User",
"Validation",
"ValidationMode",
@@ -0,0 +1,49 @@
"""UKO (Unified Knowledge Ontology) domain models.
Pydantic v2 value objects representing the parsed UKO ontology structure:
namespace prefixes, ontology nodes (classes and properties), version
metadata, and the top-level ontology container.
All models use ``frozen=True`` for immutability as required by ADR-004.
"""
from __future__ import annotations
from pydantic import BaseModel, Field
class UKOPrefix(BaseModel, frozen=True):
"""A namespace prefix mapping."""
prefix: str
uri: str
class UKONode(BaseModel, frozen=True):
"""A node in the UKO ontology."""
uri: str
rdf_type: str # e.g. "owl:Class" or "owl:ObjectProperty"
label: str = ""
comment: str = ""
parent_uris: tuple[str, ...] = () # rdfs:subClassOf targets (multi-parent)
layer: int = Field(ge=0, le=3, default=0)
properties: tuple[tuple[str, str], ...] = () # immutable key-value pairs
class UKOVersion(BaseModel, frozen=True):
"""Ontology version metadata."""
version_iri: str
version: str # e.g. "0.1.0"
label: str
comment: str
class UKOOntology(BaseModel, frozen=True):
"""Parsed UKO ontology."""
version: UKOVersion
prefixes: tuple[UKOPrefix, ...] = ()
nodes: tuple[UKONode, ...] = ()
supported_versions: tuple[str, ...] = ("0.1.0",)