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
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:
@@ -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`,
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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']}"
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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",)
|
||||
Reference in New Issue
Block a user