feat(acms): implement UKO Layer 2 Paradigm Vocabularies (uko-oo, uko-func, uko-proc)
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 15s
CI / build (pull_request) Successful in 16s
CI / quality (pull_request) Successful in 19s
CI / e2e_tests (pull_request) Successful in 23s
CI / typecheck (pull_request) Successful in 32s
CI / security (pull_request) Successful in 43s
CI / unit_tests (pull_request) Successful in 2m2s
CI / integration_tests (pull_request) Successful in 2m41s
CI / docker (pull_request) Successful in 49s
CI / coverage (pull_request) Successful in 5m32s
CI / benchmark-regression (pull_request) Successful in 36m2s

This commit is contained in:
2026-03-13 18:21:04 +00:00
parent 317d015260
commit 3c014a9565
20 changed files with 3894 additions and 130 deletions
+13
View File
@@ -43,6 +43,19 @@
real DI path. Includes Robot Framework smoke tests and ASV benchmarks.
Tests are intentionally failing (``@tdd_expected_fail``) until the bug fix
for #570 is applied. (#630)
- Implemented UKO Layer 2 paradigm vocabulary specializations: Object-Oriented
(`uko-oo:`), Functional (`uko-func:`), and Procedural (`uko-proc:`). Added
OWL/Turtle class and property definitions for all three paradigms in
`docs/ontology/uko.ttl`. Implemented `DetailLevelMapBuilder` with insertion
and integer reassignment logic for extending parent DetailLevelMaps.
`ParadigmVocabulary`, `VocabularyClass`, `VocabularyProperty`, and
`VocabularyRegistry` frozen Pydantic models provide the Python API. Includes
Behave BDD tests (60+ scenarios), Robot Framework integration helper, ASV
benchmarks for DetailLevelMap operations, and reference documentation.
**Breaking:** `DetailLevelMap.effective_levels()` now returns
`MappingProxyType[str, int]` (read-only) instead of `dict[str, int]`;
callers that mutated the returned mapping must copy to a `dict` first.
(#575, PR #657)
- Added `RepoIndexingService` for repository file indexing with incremental
refresh, extension-based language detection, SHA-256 content hashing, and
token estimation. Supports policy enforcement via include/exclude globs,
+142
View File
@@ -0,0 +1,142 @@
"""ASV benchmarks for DetailLevelMap operations.
Measures the performance of DetailLevelMap operations including:
- Named level resolution (string lookups)
- Integer depth resolution (clamping)
- Effective level map computation (inheritance merge)
- DetailLevelMapBuilder insertions and build
- VocabularyRegistry lookups
Based on specification.md DetailDepth and DetailLevelMap sections.
"""
from __future__ import annotations
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)
from cleveragents.acms.uko.detail_level_maps import (
CODE_DETAIL_LEVEL_MAP,
OO_DETAIL_LEVEL_MAP,
DetailLevelMapBuilder,
build_effective_map,
)
from cleveragents.acms.uko.vocabularies import (
get_func_vocabulary,
get_oo_vocabulary,
get_proc_vocabulary,
)
from cleveragents.acms.uko.vocabulary_registry import VocabularyRegistry
class DetailLevelMapResolutionSuite:
"""Benchmark DetailLevelMap.resolve() for named and integer depths."""
timeout = 60
def setup(self) -> None:
"""Prepare maps for benchmarking."""
self.code_map = CODE_DETAIL_LEVEL_MAP
self.oo_map = OO_DETAIL_LEVEL_MAP
def time_resolve_named_level_code(self) -> None:
"""Benchmark resolving a named level in the code map."""
self.code_map.resolve("SIGNATURES")
def time_resolve_named_level_oo(self) -> None:
"""Benchmark resolving a named level in the OO map."""
self.oo_map.resolve("CLASS_HIERARCHY")
def time_resolve_integer_depth(self) -> None:
"""Benchmark resolving an integer depth."""
self.code_map.resolve(4)
def time_resolve_integer_clamped(self) -> None:
"""Benchmark resolving an integer that exceeds max_depth."""
self.code_map.resolve(100)
def time_resolve_inherited_level(self) -> None:
"""Benchmark resolving a level inherited from parent."""
self.oo_map.resolve("MODULE_LISTING")
def time_effective_levels_code(self) -> None:
"""Benchmark computing effective levels for code map."""
self.code_map.effective_levels()
def time_effective_levels_oo(self) -> None:
"""Benchmark computing effective levels for OO map (with parent)."""
self.oo_map.effective_levels()
class DetailLevelMapBuilderSuite:
"""Benchmark DetailLevelMapBuilder operations."""
timeout = 60
def setup(self) -> None:
"""Prepare parent map."""
self.code_map = CODE_DETAIL_LEVEL_MAP
def time_build_with_single_insertion(self) -> None:
"""Benchmark building a map with one insertion."""
builder = DetailLevelMapBuilder(self.code_map, "bench:")
builder.insert_after("MEMBER_LISTING", "BENCH_LEVEL")
builder.build()
def time_build_with_two_insertions(self) -> None:
"""Benchmark building a map with two insertions (OO pattern)."""
builder = DetailLevelMapBuilder(self.code_map, "bench:")
builder.insert_after("MEMBER_LISTING", "BENCH_A")
builder.insert_after("SIGNATURES_WITH_DOCS", "BENCH_B")
builder.build()
def time_build_effective_map_no_insertions(self) -> None:
"""Benchmark build_effective_map with empty insertions."""
build_effective_map(self.code_map, [])
def time_build_effective_map_with_insertions(self) -> None:
"""Benchmark build_effective_map with two insertions."""
build_effective_map(
self.code_map,
[("MEMBER_LISTING", "X"), ("SIGNATURES_WITH_DOCS", "Y")],
)
class VocabularyRegistrySuite:
"""Benchmark VocabularyRegistry operations."""
timeout = 60
def setup(self) -> None:
"""Build and populate the registry."""
self.registry = VocabularyRegistry(
vocabularies=(
get_oo_vocabulary(),
get_func_vocabulary(),
get_proc_vocabulary(),
)
)
def time_get_by_prefix(self) -> None:
"""Benchmark looking up vocabulary by prefix."""
self.registry.get_by_prefix("uko-oo:")
def time_get_by_iri(self) -> None:
"""Benchmark looking up vocabulary by IRI."""
self.registry.get_by_iri("https://cleveragents.ai/ontology/uko/oo#")
def time_list_prefixes(self) -> None:
"""Benchmark listing all prefixes."""
self.registry.list_prefixes()
def time_list_all(self) -> None:
"""Benchmark listing all vocabularies."""
self.registry.list_all()
def time_contains_check(self) -> None:
"""Benchmark __contains__ check."""
_ = "uko-oo:" in self.registry
+56 -3
View File
@@ -4,7 +4,8 @@
@prefix uko-data: <https://cleveragents.ai/ontology/uko/data#> .
@prefix uko-infra: <https://cleveragents.ai/ontology/uko/infra#> .
@prefix uko-oo: <https://cleveragents.ai/ontology/uko/oo#> .
@prefix uko-py: <https://cleveragents.ai/ontology/uko/py#> .
@prefix uko-func: <https://cleveragents.ai/ontology/uko/func#> .
@prefix uko-proc: <https://cleveragents.ai/ontology/uko/proc#> .
@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#> .
@@ -551,6 +552,10 @@ uko-infra:exposes a owl:ObjectProperty ;
# Layer 2: Object-Oriented Paradigm (uko-oo:)
# ===========================================================================
# Design decision: uko-oo:Class inherits from both TypeDefinition (an
# Atom descendant) and Container. This is intentional OWL multiple
# inheritance per specification.md ~line 44340. Atom and Container
# are NOT declared owl:disjointWith, so individuals are satisfiable.
uko-oo:Class a owl:Class ;
rdfs:subClassOf uko-code:TypeDefinition , uko:Container ;
rdfs:label "Class" ;
@@ -585,10 +590,58 @@ uko-oo:implements a owl:ObjectProperty ;
rdfs:range uko-oo:Interface ;
rdfs:label "implements" .
# ===========================================================================
# Layer 2: Functional Paradigm (uko-func:)
# ===========================================================================
uko-func:PureFunction a owl:Class ;
rdfs:subClassOf uko-code:Callable ;
rdfs:label "PureFunction" ;
rdfs:comment "A pure function with no side effects." .
uko-func:TypeClass a owl:Class ;
rdfs:subClassOf uko-code:TypeDefinition , uko:Boundary ;
rdfs:label "TypeClass" ;
rdfs:comment "A type class defining a set of operations for types." .
uko-func:Monad a owl:Class ;
rdfs:subClassOf uko-code:TypeDefinition ;
rdfs:label "Monad" ;
rdfs:comment "A monadic type encapsulating computation context." .
# ===========================================================================
# Layer 2: Procedural Paradigm (uko-proc:)
# ===========================================================================
uko-proc:ProceduralFunction a owl:Class ;
rdfs:subClassOf uko-code:Callable ;
rdfs:label "ProceduralFunction" ;
rdfs:comment "A procedural function declaration." .
uko-proc:GlobalVariable a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "GlobalVariable" ;
rdfs:comment "A global variable declaration." .
uko-proc:HeaderFile a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "HeaderFile" ;
rdfs:comment "A header file defining exported declarations." .
uko-proc:StructDefinition a owl:Class ;
rdfs:subClassOf uko-code:TypeDefinition ;
rdfs:label "StructDefinition" ;
rdfs:comment "A C-style struct type definition." .
uko-proc:Macro a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Macro" ;
rdfs:comment "A preprocessor macro definition." .
# ===========================================================================
# 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.
# rather than new OWL classes. Technology-specific prefixes (e.g. uko-py:)
# will be declared when their DetailLevelMap support is added.
@@ -0,0 +1,171 @@
# UKO Layer 2 Paradigm Vocabularies
## Overview
Layer 2 of the Unified Knowledge Ontology (UKO) refines the Layer 1
`uko-code:` domain with paradigm-specific specializations for three
programming paradigms:
- **Object-Oriented** (`uko-oo:`) — classes, interfaces, methods, attributes
- **Functional** (`uko-func:`) — pure functions, type classes, monads
- **Procedural** (`uko-proc:`) — procedural functions, globals, headers, structs, macros
Each paradigm defines OWL classes that subclass Layer 1 or Layer 0 concepts,
and extends the parent `uko-code:` DetailLevelMap with paradigm-specific
named levels.
Based on `docs/specification.md` ~lines 42333-42422, 24923-25027.
## Namespace Prefixes
| Prefix | IRI | Layer |
|--------|-----|-------|
| `uko-oo:` | `https://cleveragents.ai/ontology/uko/oo#` | 2 |
| `uko-func:` | `https://cleveragents.ai/ontology/uko/func#` | 2 |
| `uko-proc:` | `https://cleveragents.ai/ontology/uko/proc#` | 2 |
## Object-Oriented Vocabulary (`uko-oo:`)
### Classes
| Class | Superclasses | Description |
|-------|-------------|-------------|
| `uko-oo:Class` | `uko-code:TypeDefinition`, `uko:Container` | An OO class that contains methods and attributes. |
| `uko-oo:Interface` | `uko-code:TypeDefinition`, `uko:Boundary` | An interface or abstract base class. |
| `uko-oo:Method` | `uko-code:Callable` | A method defined within a class. |
| `uko-oo:Attribute` | `uko:Atom` | A class or instance attribute. |
### Properties
| Property | Domain | Range | subPropertyOf |
|----------|--------|-------|---------------|
| `uko-oo:inheritsFrom` | `uko-oo:Class` | `uko-oo:Class` | `uko:dependsOn` |
| `uko-oo:implements` | `uko-oo:Class` | `uko-oo:Interface` | `uko:dependsOn` |
### DetailLevelMap Insertions
The OO map inserts two levels into the `uko-code:` base map:
| Depth | Name | Origin | Content |
|-------|------|--------|---------|
| 3 | `CLASS_HIERARCHY` | **uko-oo:** | Inheritance chains and interface implementations |
| 7 | `VISIBILITY_ANNOTATED` | **uko-oo:** | Public/protected/private modifiers |
The full effective map with reassigned depths is documented in
`docs/specification.md` ~lines 24991-25004.
## Functional Vocabulary (`uko-func:`)
### Classes
| Class | Superclasses | Description |
|-------|-------------|-------------|
| `uko-func:PureFunction` | `uko-code:Callable` | A pure function with no side effects. |
| `uko-func:TypeClass` | `uko-code:TypeDefinition`, `uko:Boundary` | A type class defining a set of operations for types. |
| `uko-func:Monad` | `uko-code:TypeDefinition` | A monadic type encapsulating computation context. |
### DetailLevelMap
Uses the same named levels as `uko-code:` without insertions.
## Procedural Vocabulary (`uko-proc:`)
### Classes
| Class | Superclasses | Description |
|-------|-------------|-------------|
| `uko-proc:ProceduralFunction` | `uko-code:Callable` | A procedural function declaration. |
| `uko-proc:GlobalVariable` | `uko:Atom` | A global variable declaration. |
| `uko-proc:HeaderFile` | `uko:Container` | A header file defining exported declarations. |
| `uko-proc:StructDefinition` | `uko-code:TypeDefinition` | A C-style struct type definition. |
| `uko-proc:Macro` | `uko:Atom` | A preprocessor macro definition. |
### DetailLevelMap
Uses the same named levels as `uko-code:` without insertions.
## DetailLevelMap Inheritance
The inheritance chain resolves named levels by walking up the map hierarchy:
```
Layer 3 (uko-py:) -> Layer 2 (uko-oo:) -> Layer 1 (uko-code:) -> Layer 0 (uko:)
```
When a named level is requested, the system looks up the name in the most
specific map first, then walks up to parent maps until a match is found.
Integer depths are used directly (clamped to `max_depth`).
### Insertion Mechanics
When a child map inserts a new named level (e.g., `CLASS_HIERARCHY` after
`MEMBER_LISTING`), all subsequent levels shift upward by one to maintain
consecutive integer numbering.
## Python API
### `DetailLevelMapBuilder`
```python
from cleveragents.acms.uko.detail_level_maps import (
CODE_DETAIL_LEVEL_MAP,
DetailLevelMapBuilder,
)
builder = DetailLevelMapBuilder(CODE_DETAIL_LEVEL_MAP, "uko-oo:")
builder.insert_after("MEMBER_LISTING", "CLASS_HIERARCHY")
builder.insert_after("SIGNATURES_WITH_DOCS", "VISIBILITY_ANNOTATED")
oo_map = builder.build()
assert oo_map.resolve("CLASS_HIERARCHY") == 3
assert oo_map.resolve("FULL_SOURCE") == 11
```
### `VocabularyRegistry`
```python
from cleveragents.acms.uko.vocabularies import (
get_oo_vocabulary,
get_func_vocabulary,
get_proc_vocabulary,
)
from cleveragents.acms.uko.vocabulary_registry import VocabularyRegistry
registry = VocabularyRegistry(
vocabularies=(
get_oo_vocabulary(),
get_func_vocabulary(),
get_proc_vocabulary(),
)
)
oo = registry.get_by_prefix("uko-oo:")
assert oo is not None
assert oo.get_class("Class") is not None
```
## Parent-Layer Dependencies
Layer 2 vocabularies reference these Layer 1 URIs via `rdfs:subClassOf`:
- `https://cleveragents.ai/ontology/uko/code#TypeDefinition`
- `https://cleveragents.ai/ontology/uko/code#Callable`
Layer 2 vocabularies also reference these Layer 0 URIs via
`rdfs:subClassOf` or `rdfs:subPropertyOf`:
- `https://cleveragents.ai/ontology/uko#Container` — superclass of `uko-oo:Class`, `uko-proc:HeaderFile`
- `https://cleveragents.ai/ontology/uko#Atom` — superclass of `uko-oo:Attribute`, `uko-proc:GlobalVariable`, `uko-proc:Macro`
- `https://cleveragents.ai/ontology/uko#Boundary` — superclass of `uko-oo:Interface`, `uko-func:TypeClass`
- `https://cleveragents.ai/ontology/uko#dependsOn` — superproperty of `uko-oo:inheritsFrom`, `uko-oo:implements`
All URIs are defined in `docs/ontology/uko.ttl` and will be fully wired
when the Layer 1 Python module (`uko-code:`) is implemented.
## Limitations
- Layer 1 (`uko-code:`) Python module is not yet implemented; the Python
code works standalone with the `DetailLevelMap` from `crp.py`.
- Turtle files are validated by the `UKOLoader` but not by a full
OWL reasoner.
- The procedural vocabulary classes are inferred from the spec's rendering
tables rather than explicit Turtle definitions.
@@ -0,0 +1,766 @@
@phase2 @acms @uko_layer2
Feature: UKO Layer 2 Paradigm Vocabularies
As a CleverAgents developer
I want paradigm-specific UKO vocabulary specializations
So that OO, functional, and procedural code concepts have precise ontology types
# ---------------------------------------------------------------------------
# VocabularyClass — Construction and Validation
# ---------------------------------------------------------------------------
@vocab_class
Scenario: Create a VocabularyClass for uko_l2
Given a simple vocabulary class with uri "https://cleveragents.ai/ontology/uko/oo#Class" and label "Class" for uko_l2
Then the vocabulary class uri should be "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
And the vocabulary class label should be "Class" for uko_l2
And the vocabulary class comment should be empty for uko_l2
And the vocabulary class parent_uris should be empty for uko_l2
@vocab_class
Scenario: Create a VocabularyClass with parents for uko_l2
Given a vocabulary class with uri "https://cleveragents.ai/ontology/uko/oo#Class" and label "Class" and parents "https://cleveragents.ai/ontology/uko/code#TypeDefinition,https://cleveragents.ai/ontology/uko#Container" for uko_l2
Then the vocabulary class should have 2 parent URIs for uko_l2
And the vocabulary class parent 0 should be "https://cleveragents.ai/ontology/uko/code#TypeDefinition" for uko_l2
@vocab_class @validation
Scenario: Reject VocabularyClass with empty URI for uko_l2
When I create a vocabulary class with empty uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_class @validation
Scenario: Reject VocabularyClass with whitespace-only URI for uko_l2
When I create a vocabulary class with whitespace uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_class @validation
Scenario: Reject VocabularyClass with empty label for uko_l2
When I create a vocabulary class with empty label for uko_l2
Then a validation error should be raised for uko_l2
@vocab_class @vocab_frozen
Scenario: VocabularyClass is immutable for uko_l2
Given a simple vocabulary class with uri "https://cleveragents.ai/ontology/uko/oo#Class" and label "Class" for uko_l2
When I try to mutate the vocabulary class uri for uko_l2
Then a frozen mutation error should be raised for uko_l2
# ---------------------------------------------------------------------------
# VocabularyProperty — Construction and Validation
# ---------------------------------------------------------------------------
@vocab_property
Scenario: Create a VocabularyProperty for uko_l2
Given a basic vocabulary property with uri "https://cleveragents.ai/ontology/uko/oo#inheritsFrom" and label "inheritsFrom" and domain "https://cleveragents.ai/ontology/uko/oo#Class" and range "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
Then the vocabulary property uri should be "https://cleveragents.ai/ontology/uko/oo#inheritsFrom" for uko_l2
And the vocabulary property label should be "inheritsFrom" for uko_l2
And the vocabulary property sub_property_of should be empty for uko_l2
@vocab_property
Scenario: Create a VocabularyProperty with subPropertyOf for uko_l2
Given a vocabulary property with uri "https://cleveragents.ai/ontology/uko/oo#inheritsFrom" and label "inheritsFrom" and domain "https://cleveragents.ai/ontology/uko/oo#Class" and range "https://cleveragents.ai/ontology/uko/oo#Class" and sub_property_of "https://cleveragents.ai/ontology/uko#dependsOn" for uko_l2
Then the vocabulary property sub_property_of should be "https://cleveragents.ai/ontology/uko#dependsOn" for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with empty URI for uko_l2
When I create a vocabulary property with empty uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @vocab_frozen
Scenario: VocabularyProperty is immutable for uko_l2
Given a basic vocabulary property with uri "https://cleveragents.ai/ontology/uko/oo#inheritsFrom" and label "inheritsFrom" and domain "https://cleveragents.ai/ontology/uko/oo#Class" and range "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
When I try to mutate the vocabulary property uri for uko_l2
Then a frozen mutation error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Object-Oriented Vocabulary (uko-oo:)
# ---------------------------------------------------------------------------
@uko_oo
Scenario: uko-oo vocabulary has all required classes for uko_l2
Given the uko-oo vocabulary for uko_l2
Then the vocabulary should have prefix "uko-oo:" for uko_l2
And the vocabulary should have IRI "https://cleveragents.ai/ontology/uko/oo#" for uko_l2
And the vocabulary should have layer 2 for uko_l2
And the vocabulary should have 4 classes for uko_l2
And the vocabulary should contain class "Class" for uko_l2
And the vocabulary should contain class "Interface" for uko_l2
And the vocabulary should contain class "Method" for uko_l2
And the vocabulary should contain class "Attribute" for uko_l2
@uko_oo
Scenario: uko-oo vocabulary has all required properties for uko_l2
Given the uko-oo vocabulary for uko_l2
Then the vocabulary should have 2 properties for uko_l2
And the vocabulary should contain property "inheritsFrom" for uko_l2
And the vocabulary should contain property "implements" for uko_l2
@uko_oo
Scenario: uko-oo Class has correct parent URIs for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up class "Class" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition" for uko_l2
And the class should have parent "https://cleveragents.ai/ontology/uko#Container" for uko_l2
@uko_oo
Scenario: uko-oo Interface has correct parent URIs for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up class "Interface" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition" for uko_l2
And the class should have parent "https://cleveragents.ai/ontology/uko#Boundary" for uko_l2
@uko_oo
Scenario: uko-oo Method has correct parent URI for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up class "Method" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#Callable" for uko_l2
@uko_oo
Scenario: uko-oo Attribute has correct parent URI for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up class "Attribute" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko#Atom" for uko_l2
@uko_oo
Scenario: uko-oo inheritsFrom property has correct domain and range for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up property "inheritsFrom" for uko_l2
Then the property domain should be "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
And the property range should be "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
And the property sub_property_of should be "https://cleveragents.ai/ontology/uko#dependsOn" for uko_l2
@uko_oo
Scenario: uko-oo implements property has correct domain and range for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up property "implements" for uko_l2
Then the property domain should be "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
And the property range should be "https://cleveragents.ai/ontology/uko/oo#Interface" for uko_l2
# ---------------------------------------------------------------------------
# Functional Vocabulary (uko-func:)
# ---------------------------------------------------------------------------
@uko_func
Scenario: uko-func vocabulary has all required classes for uko_l2
Given the uko-func vocabulary for uko_l2
Then the vocabulary should have prefix "uko-func:" for uko_l2
And the vocabulary should have IRI "https://cleveragents.ai/ontology/uko/func#" for uko_l2
And the vocabulary should have layer 2 for uko_l2
And the vocabulary should have 3 classes for uko_l2
And the vocabulary should contain class "PureFunction" for uko_l2
And the vocabulary should contain class "TypeClass" for uko_l2
And the vocabulary should contain class "Monad" for uko_l2
@uko_func
Scenario: uko-func vocabulary has no properties for uko_l2
Given the uko-func vocabulary for uko_l2
Then the vocabulary should have 0 properties for uko_l2
@uko_func
Scenario: uko-func PureFunction has correct parent URI for uko_l2
Given the uko-func vocabulary for uko_l2
When I look up class "PureFunction" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#Callable" for uko_l2
@uko_func
Scenario: uko-func TypeClass has correct parent URIs for uko_l2
Given the uko-func vocabulary for uko_l2
When I look up class "TypeClass" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition" for uko_l2
And the class should have parent "https://cleveragents.ai/ontology/uko#Boundary" for uko_l2
@uko_func
Scenario: uko-func Monad has correct parent URI for uko_l2
Given the uko-func vocabulary for uko_l2
When I look up class "Monad" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition" for uko_l2
# ---------------------------------------------------------------------------
# Procedural Vocabulary (uko-proc:)
# ---------------------------------------------------------------------------
@uko_proc
Scenario: uko-proc vocabulary has all required classes for uko_l2
Given the uko-proc vocabulary for uko_l2
Then the vocabulary should have prefix "uko-proc:" for uko_l2
And the vocabulary should have IRI "https://cleveragents.ai/ontology/uko/proc#" for uko_l2
And the vocabulary should have layer 2 for uko_l2
And the vocabulary should have 5 classes for uko_l2
And the vocabulary should contain class "ProceduralFunction" for uko_l2
And the vocabulary should contain class "GlobalVariable" for uko_l2
And the vocabulary should contain class "HeaderFile" for uko_l2
And the vocabulary should contain class "StructDefinition" for uko_l2
And the vocabulary should contain class "Macro" for uko_l2
@uko_proc
Scenario: uko-proc vocabulary has no properties for uko_l2
Given the uko-proc vocabulary for uko_l2
Then the vocabulary should have 0 properties for uko_l2
@uko_proc
Scenario: uko-proc ProceduralFunction has correct parent URI for uko_l2
Given the uko-proc vocabulary for uko_l2
When I look up class "ProceduralFunction" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#Callable" for uko_l2
@uko_proc
Scenario: uko-proc GlobalVariable has correct parent URI for uko_l2
Given the uko-proc vocabulary for uko_l2
When I look up class "GlobalVariable" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko#Atom" for uko_l2
@uko_proc
Scenario: uko-proc HeaderFile has correct parent URI for uko_l2
Given the uko-proc vocabulary for uko_l2
When I look up class "HeaderFile" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko#Container" for uko_l2
@uko_proc
Scenario: uko-proc StructDefinition has correct parent URI for uko_l2
Given the uko-proc vocabulary for uko_l2
When I look up class "StructDefinition" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko/code#TypeDefinition" for uko_l2
@uko_proc
Scenario: uko-proc Macro has correct parent URI for uko_l2
Given the uko-proc vocabulary for uko_l2
When I look up class "Macro" for uko_l2
Then the class should have parent "https://cleveragents.ai/ontology/uko#Atom" for uko_l2
# ---------------------------------------------------------------------------
# ParadigmVocabulary — Lookup helpers
# ---------------------------------------------------------------------------
@vocab_lookup
Scenario: Look up class by label for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up class "Class" for uko_l2
Then the looked up class should not be None for uko_l2
And the looked up class uri should be "https://cleveragents.ai/ontology/uko/oo#Class" for uko_l2
@vocab_lookup
Scenario: Look up missing class returns None for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up class "NonExistent" for uko_l2
Then the looked up class should be None for uko_l2
@vocab_lookup
Scenario: Look up property by label for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up property "inheritsFrom" for uko_l2
Then the looked up property should not be None for uko_l2
@vocab_lookup
Scenario: Look up missing property returns None for uko_l2
Given the uko-oo vocabulary for uko_l2
When I look up property "nonExistent" for uko_l2
Then the looked up property should be None for uko_l2
@vocab_lookup
Scenario: class_uris returns all class URIs for uko_l2
Given the uko-oo vocabulary for uko_l2
Then class_uris should have 4 entries for uko_l2
@vocab_lookup
Scenario: property_uris returns all property URIs for uko_l2
Given the uko-oo vocabulary for uko_l2
Then property_uris should have 2 entries for uko_l2
# ---------------------------------------------------------------------------
# VocabularyRegistry
# ---------------------------------------------------------------------------
@vocab_registry
Scenario: Register and look up vocabularies for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
Then the registry should have 3 vocabularies for uko_l2
And the registry should contain prefix "uko-oo:" for uko_l2
And the registry should contain prefix "uko-func:" for uko_l2
And the registry should contain prefix "uko-proc:" for uko_l2
@vocab_registry
Scenario: Look up vocabulary by prefix for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I look up vocabulary by prefix "uko-oo:" for uko_l2
Then the looked up vocabulary should not be None for uko_l2
And the looked up vocabulary prefix should be "uko-oo:" for uko_l2
@vocab_registry
Scenario: Look up vocabulary by IRI for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I look up vocabulary by IRI "https://cleveragents.ai/ontology/uko/func#" for uko_l2
Then the looked up vocabulary should not be None for uko_l2
And the looked up vocabulary prefix should be "uko-func:" for uko_l2
@vocab_registry
Scenario: Look up missing prefix returns None for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I look up vocabulary by prefix "uko-unknown:" for uko_l2
Then the looked up vocabulary should be None for uko_l2
@vocab_registry @validation
Scenario: Reject duplicate vocabulary registration for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I try to register a duplicate vocabulary for uko_l2
Then a duplicate registration error should be raised for uko_l2
@vocab_registry
Scenario: list_prefixes returns sorted prefixes for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
Then list_prefixes should return "uko-func:,uko-oo:,uko-proc:" for uko_l2
@vocab_registry
Scenario: list_all returns all vocabularies for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
Then list_all should return 3 vocabularies for uko_l2
@vocab_registry
Scenario: Registry contains check for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
Then "uko-oo:" should be in the registry for uko_l2
And "uko-missing:" should not be in the registry for uko_l2
@vocab_registry
Scenario: Registry contains check supports IRI for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
Then "https://cleveragents.ai/ontology/uko/oo#" should be in the registry for uko_l2
@vocab_registry
Scenario: Unregister vocabulary from registry for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I unregister prefix "uko-oo:" from the registry for uko_l2
Then the registry should have 2 vocabularies for uko_l2
And "uko-oo:" should not be in the registry for uko_l2
@vocab_registry
Scenario: Unregister missing prefix returns False for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I unregister prefix "uko-missing:" from the registry for uko_l2
Then the unregister result should be False for uko_l2
# ---------------------------------------------------------------------------
# DetailLevelMap — Layer 1 base map (uko-code:)
# ---------------------------------------------------------------------------
@detail_level_map
Scenario: uko-code base map has correct levels for uko_l2
Given the uko-code detail level map for uko_l2
Then the map should have domain "uko-code:" for uko_l2
And the map should have max_depth 9 for uko_l2
And the map should resolve "MODULE_LISTING" to 0 for uko_l2
And the map should resolve "FULL_SOURCE" to 9 for uko_l2
And the map should resolve "SIGNATURES" to 4 for uko_l2
@detail_level_map
Scenario: uko-code base map has 10 named levels for uko_l2
Given the uko-code detail level map for uko_l2
Then the map should have 10 levels for uko_l2
# ---------------------------------------------------------------------------
# DetailLevelMap — uko-oo: with insertions
# ---------------------------------------------------------------------------
@detail_level_map @uko_oo
Scenario: uko-oo map inserts CLASS_HIERARCHY at depth 3 for uko_l2
Given the uko-oo detail level map for uko_l2
Then the map should have domain "uko-oo:" for uko_l2
And the map should resolve "CLASS_HIERARCHY" to 3 for uko_l2
@detail_level_map @uko_oo
Scenario: uko-oo map inserts VISIBILITY_ANNOTATED at depth 7 for uko_l2
Given the uko-oo detail level map for uko_l2
Then the map should resolve "VISIBILITY_ANNOTATED" to 7 for uko_l2
@detail_level_map @uko_oo
Scenario: uko-oo map shifts subsequent depths for uko_l2
Given the uko-oo detail level map for uko_l2
Then the map should resolve "MEMBER_SUMMARY" to 4 for uko_l2
And the map should resolve "SIGNATURES" to 5 for uko_l2
And the map should resolve "SIGNATURES_WITH_DOCS" to 6 for uko_l2
And the map should resolve "STRUCTURAL_OUTLINE" to 8 for uko_l2
And the map should resolve "FULL_SOURCE" to 11 for uko_l2
@detail_level_map @uko_oo
Scenario: uko-oo map has 12 total levels for uko_l2
Given the uko-oo detail level map for uko_l2
Then the map should have 12 levels for uko_l2
And the map should have max_depth 11 for uko_l2
@detail_level_map @uko_oo
Scenario: uko-oo map resolves integer depths for uko_l2
Given the uko-oo detail level map for uko_l2
Then the map should resolve integer 0 to 0 for uko_l2
And the map should resolve integer 5 to 5 for uko_l2
And the map should resolve integer 100 to 11 for uko_l2
@detail_level_map @uko_oo
Scenario: uko-oo map inherits from uko-code parent for uko_l2
Given the uko-oo detail level map for uko_l2
Then the map parent domain should be "uko-code:" for uko_l2
# ---------------------------------------------------------------------------
# DetailLevelMap — uko-func:
# ---------------------------------------------------------------------------
@detail_level_map @uko_func
Scenario: uko-func map has same levels as uko-code for uko_l2
Given the uko-func detail level map for uko_l2
Then the map should have domain "uko-func:" for uko_l2
And the map should have 10 levels for uko_l2
And the map should have max_depth 9 for uko_l2
And the map should resolve "SIGNATURES" to 4 for uko_l2
# ---------------------------------------------------------------------------
# DetailLevelMap — uko-proc:
# ---------------------------------------------------------------------------
@detail_level_map @uko_proc
Scenario: uko-proc map has same levels as uko-code for uko_l2
Given the uko-proc detail level map for uko_l2
Then the map should have domain "uko-proc:" for uko_l2
And the map should have 10 levels for uko_l2
And the map should have max_depth 9 for uko_l2
And the map should resolve "SIGNATURES_WITH_DOCS" to 5 for uko_l2
# ---------------------------------------------------------------------------
# DetailLevelMapBuilder — Construction and edge cases
# ---------------------------------------------------------------------------
@detail_level_map_builder
Scenario: Build a child map with single insertion for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I insert "TEST_LEVEL" after "MEMBER_LISTING" for uko_l2
And I build the map for uko_l2
Then the built map should have domain "test:" for uko_l2
And the built map should resolve "TEST_LEVEL" to 3 for uko_l2
And the built map should resolve "MEMBER_SUMMARY" to 4 for uko_l2
And the built map should have 11 levels for uko_l2
@detail_level_map_builder @validation
Scenario: Reject insertion after non-existent level for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I try to insert after non-existent level "BOGUS" for uko_l2
Then a value error should be raised for uko_l2
@detail_level_map_builder @validation
Scenario: Reject builder with empty domain for uko_l2
When I try to create a builder with empty domain for uko_l2
Then a value error should be raised for uko_l2
@detail_level_map_builder @validation
Scenario: Reject insertion with empty new_level name for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I try to insert empty level name after "MEMBER_LISTING" for uko_l2
Then a value error should be raised for uko_l2
@detail_level_map_builder @validation
Scenario: Reject insertion with empty after_level name for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I try to insert "TEST" after empty level name for uko_l2
Then a value error should be raised for uko_l2
@detail_level_map_builder
Scenario: Build with multiple insertions for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I insert "FIRST" after "MODULE_LISTING" for uko_l2
And I insert "SECOND" after "FIRST" for uko_l2
And I build the map for uko_l2
Then the built map should resolve "FIRST" to 1 for uko_l2
And the built map should resolve "SECOND" to 2 for uko_l2
And the built map should resolve "MODULE_GRAPH" to 3 for uko_l2
@detail_level_map_builder
Scenario: Builder exposes parent and domain properties for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
Then the builder parent domain should be "uko-code:" for uko_l2
And the builder domain should be "test:" for uko_l2
# ---------------------------------------------------------------------------
# build_effective_map function
# ---------------------------------------------------------------------------
@effective_map
Scenario: Build effective map with no insertions for uko_l2
Given the uko-code detail level map for uko_l2
When I build an effective map with no insertions for uko_l2
Then the effective map should have 10 entries for uko_l2
And the effective map entry 0 should be "MODULE_LISTING" at depth 0 for uko_l2
@effective_map
Scenario: Build effective map with insertions for uko_l2
Given the uko-code detail level map for uko_l2
When I build an effective map with insertion "CLASS_HIERARCHY" after "MEMBER_LISTING" for uko_l2
Then the effective map should have 11 entries for uko_l2
And the effective map entry 3 should be "CLASS_HIERARCHY" at depth 3 for uko_l2
And the effective map entry 4 should be "MEMBER_SUMMARY" at depth 4 for uko_l2
@effective_map @validation
Scenario: build_effective_map rejects invalid after_level for uko_l2
Given the uko-code detail level map for uko_l2
When I try to build an effective map with insertion after "BOGUS" for uko_l2
Then a value error should be raised for uko_l2
# ---------------------------------------------------------------------------
# DetailLevelMap unknown level resolution
# ---------------------------------------------------------------------------
@detail_level_map @error
Scenario: Reject unknown named level for uko_l2
Given the uko-code detail level map for uko_l2
When I try to resolve unknown level "NONEXISTENT" for uko_l2
Then a value error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Turtle validation — TTL file loads successfully
# ---------------------------------------------------------------------------
@turtle @validation
Scenario: TTL file loads and validates for uko_l2
When I load the UKO TTL file for uko_l2
Then the ontology should load without errors for uko_l2
And the ontology should contain uko-func prefix for uko_l2
And the ontology should contain uko-proc prefix for uko_l2
@turtle @validation
Scenario: TTL file contains uko-func classes for uko_l2
When I load the UKO TTL file for uko_l2
Then the ontology should contain node "PureFunction" for uko_l2
And the ontology should contain node "TypeClass" for uko_l2
And the ontology should contain node "Monad" for uko_l2
@turtle @validation
Scenario: TTL file contains uko-proc classes for uko_l2
When I load the UKO TTL file for uko_l2
Then the ontology should contain node "ProceduralFunction" for uko_l2
And the ontology should contain node "GlobalVariable" for uko_l2
And the ontology should contain node "HeaderFile" for uko_l2
And the ontology should contain node "StructDefinition" for uko_l2
And the ontology should contain node "Macro" for uko_l2
@turtle @validation
Scenario: TTL file contains existing uko-oo classes for uko_l2
When I load the UKO TTL file for uko_l2
Then the ontology should contain node "Class" for uko_l2
And the ontology should contain node "Interface" for uko_l2
And the ontology should contain node "Method" for uko_l2
And the ontology should contain node "Attribute" for uko_l2
@turtle @validation
Scenario: TTL file has layer 2 nodes for uko_l2
When I load the UKO TTL file for uko_l2
Then the ontology should have at least 12 layer 2 nodes for uko_l2
# ---------------------------------------------------------------------------
# ParadigmVocabulary — frozen immutability
# ---------------------------------------------------------------------------
@vocab_frozen
Scenario: ParadigmVocabulary is immutable for uko_l2
Given the uko-oo vocabulary for uko_l2
When I try to mutate the vocabulary prefix for uko_l2
Then a frozen mutation error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Additional coverage: validation edge cases
# ---------------------------------------------------------------------------
@vocab_class @validation
Scenario: Reject ParadigmVocabulary with empty prefix for uko_l2
When I create a paradigm vocabulary with empty prefix for uko_l2
Then a validation error should be raised for uko_l2
@vocab_class @validation
Scenario: Reject ParadigmVocabulary with whitespace prefix for uko_l2
When I create a paradigm vocabulary with whitespace prefix for uko_l2
Then a validation error should be raised for uko_l2
@vocab_class @validation
Scenario: Reject ParadigmVocabulary with prefix missing colon for uko_l2
When I create a paradigm vocabulary with prefix missing colon for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with whitespace-only URI for uko_l2
When I create a vocabulary property with whitespace uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_registry
Scenario: Registry __contains__ returns False for non-string for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
Then the registry should return False for non-string contains for uko_l2
@vocab_registry @validation
Scenario: Reject duplicate IRI registration for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I try to register a vocabulary with duplicate IRI for uko_l2
Then a duplicate registration error should be raised for uko_l2
@vocab_registry
Scenario: Look up missing IRI returns None for uko_l2
Given a vocabulary registry with all paradigm vocabularies for uko_l2
When I look up vocabulary by IRI "https://nonexistent.example.org/" for uko_l2
Then the looked up vocabulary should be None for uko_l2
@detail_level_map
Scenario: DetailLevelMap register adds new level for uko_l2
Given a fresh detail level map for uko_l2
When I register level "CUSTOM" with value 5 for uko_l2
Then the fresh map should resolve "CUSTOM" to 5 for uko_l2
@detail_level_map @validation
Scenario: DetailLevelMap register rejects negative depth for uko_l2
Given a fresh detail level map for uko_l2
When I try to register level "BAD" with value -1 for uko_l2
Then a value error should be raised for uko_l2
@detail_level_map
Scenario: DetailLevelMap levels are immutable MappingProxy for uko_l2
Given the uko-code detail level map for uko_l2
When I try to mutate the levels dict for uko_l2
Then a type error should be raised for uko_l2
@detail_level_map_builder @validation
Scenario: Reject insertion of duplicate level name for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I try to insert duplicate level "MODULE_LISTING" after "MEMBER_LISTING" for uko_l2
Then a value error should be raised for uko_l2
@effective_map @validation
Scenario: build_effective_map rejects duplicate level name for uko_l2
Given the uko-code detail level map for uko_l2
When I try to build an effective map with duplicate level for uko_l2
Then a value error should be raised for uko_l2
@detail_level_map
Scenario: effective_levels returns immutable mapping for uko_l2
Given the uko-code detail level map for uko_l2
When I try to mutate effective_levels for uko_l2
Then a type error should be raised for uko_l2
@detail_level_map @uko_func
Scenario: uko-func map resolves levels via parent inheritance for uko_l2
Given the uko-func detail level map for uko_l2
Then the map should resolve "MODULE_LISTING" to 0 for uko_l2
And the map should resolve "FULL_SOURCE" to 9 for uko_l2
# ---------------------------------------------------------------------------
# Review findings: M8 — empty label/domain/range rejection
# ---------------------------------------------------------------------------
@vocab_property @validation
Scenario: Reject VocabularyProperty with empty label for uko_l2
When I create a vocabulary property with empty label for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with empty domain for uko_l2
When I create a vocabulary property with empty domain for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with empty range for uko_l2
When I create a vocabulary property with empty range for uko_l2
Then a validation error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Review findings: M9 — invalid URI scheme rejection
# ---------------------------------------------------------------------------
@vocab_class @validation
Scenario: Reject VocabularyClass with non-HTTP URI for uko_l2
When I create a vocabulary class with non-http uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with non-HTTP URI for uko_l2
When I create a vocabulary property with non-http uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_class @validation
Scenario: Reject VocabularyClass with non-HTTP parent URI for uko_l2
When I create a vocabulary class with non-http parent uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with non-HTTP domain URI for uko_l2
When I create a vocabulary property with non-http domain uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with non-HTTP range URI for uko_l2
When I create a vocabulary property with non-http range uri for uko_l2
Then a validation error should be raised for uko_l2
@vocab_property @validation
Scenario: Reject VocabularyProperty with non-HTTP sub_property_of for uko_l2
When I create a vocabulary property with non-http sub_property_of for uko_l2
Then a validation error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Review findings: M10 — insert_after at tail of level list
# ---------------------------------------------------------------------------
@detail_level_map_builder
Scenario: Insert after last level FULL_SOURCE for uko_l2
Given a detail level map builder with parent "uko-code:" and domain "test:" for uko_l2
When I insert "BEYOND_SOURCE" after "FULL_SOURCE" for uko_l2
And I build the map for uko_l2
Then the built map should resolve "BEYOND_SOURCE" to 10 for uko_l2
And the built map should resolve "FULL_SOURCE" to 9 for uko_l2
And the built map should have 11 levels for uko_l2
# ---------------------------------------------------------------------------
# Review findings: H3 — MappingProxyType serialization
# ---------------------------------------------------------------------------
@detail_level_map
Scenario: DetailLevelMap model_dump_json succeeds with MappingProxy for uko_l2
Given the uko-code detail level map for uko_l2
When I serialize the detail level map to JSON for uko_l2
Then the serialized JSON should contain "MODULE_LISTING" for uko_l2
@detail_level_map
Scenario: DetailLevelMap model_dump succeeds with MappingProxy for uko_l2
Given the uko-code detail level map for uko_l2
When I dump the detail level map to dict for uko_l2
Then the dumped dict levels should contain "MODULE_LISTING" for uko_l2
# ---------------------------------------------------------------------------
# Review findings: M5 — cycle guard in resolve()
# ---------------------------------------------------------------------------
@detail_level_map @error
Scenario: Detect cycle in parent chain during resolve for uko_l2
Given a detail level map with circular parent for uko_l2
When I try to resolve level "ANYTHING" on the circular map for uko_l2
Then a cycle error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Review findings: H2 — levels field re-frozen on assignment
# ---------------------------------------------------------------------------
@detail_level_map
Scenario: Assigning dict to levels re-freezes as MappingProxy for uko_l2
Given a fresh detail level map for uko_l2
When I assign a dict to the levels field for uko_l2
Then the levels should still be an immutable MappingProxy for uko_l2
# ---------------------------------------------------------------------------
# Review findings: F7 — cycle guard in effective_levels()
# ---------------------------------------------------------------------------
@detail_level_map @error
Scenario: Detect cycle in parent chain during effective_levels for uko_l2
Given a detail level map with circular parent for uko_l2
When I try to get effective levels on the circular map for uko_l2
Then a cycle error should be raised for uko_l2
# ---------------------------------------------------------------------------
# Review findings: F13 — deepcopy support
# ---------------------------------------------------------------------------
@detail_level_map
Scenario: Deep copy of DetailLevelMap preserves data for uko_l2
Given the uko-oo detail level map for uko_l2
When I deep copy the detail level map for uko_l2
Then the copied map should have domain "uko-oo:" for uko_l2
And the copied map should resolve "CLASS_HIERARCHY" to 3 for uko_l2
And the copied map should not be the same object for uko_l2
+56
View File
@@ -0,0 +1,56 @@
"""Shared helpers for UKO Layer 2 BDD step files.
Provides a ``capture_error`` context manager that reduces the
repeated try/except boilerplate for "negative-test" steps.
Used by:
- uko_l2_vocabulary_steps.py
- uko_l2_detail_level_steps.py
- uko_l2_coverage_ttl_steps.py
- uko_l2_vocab_registry_steps.py
"""
from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from behave.runner import Context
@contextmanager
def capture_error(
ctx: Context,
*exc_types: type[BaseException],
) -> Generator[None]:
"""Run the body and capture any matching exception on ``ctx.error``.
On success ``ctx.error`` is set to ``None``; on failure it is set
to the caught exception instance.
Usage::
with capture_error(ctx, ValidationError):
VocabularyClass(uri="", label="Test")
Args:
ctx: Behave context (must support attribute assignment).
*exc_types: One or more exception types to catch.
Yields:
Control to the step body.
Note:
This intentionally suppresses the caught exception so the
subsequent ``@then`` step can inspect ``ctx.error``. This is
a **test-infrastructure exemption** from CONTRIBUTING.md's
"do not suppress errors" rule -- suppression is the entire
purpose of negative-test capture in BDD step definitions.
"""
try:
yield
ctx.error = None # type: ignore[attr-defined]
except exc_types as exc:
ctx.error = exc # type: ignore[attr-defined]
+148
View File
@@ -0,0 +1,148 @@
"""Behave steps for UKO Layer 2 TTL validation and additional coverage.
Covers Turtle file loading, ParadigmVocabulary frozen-model checks,
and miscellaneous validation edge-cases.
All step definitions use the ``for uko_l2`` suffix to avoid
AmbiguousStep collisions with other feature files.
"""
from __future__ import annotations
from pathlib import Path
from _uko_l2_test_helpers import capture_error
from behave import then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.acms.uko.vocabularies import (
ParadigmVocabulary,
VocabularyProperty,
get_oo_vocabulary,
)
from cleveragents.application.services.uko_loader import UKOLoader
_TTL_PATH = Path(__file__).resolve().parents[2] / "docs" / "ontology" / "uko.ttl"
# ---------------------------------------------------------------------------
# Turtle validation
# ---------------------------------------------------------------------------
@when("I load the UKO TTL file for uko_l2")
def step_load_ttl(ctx: Context) -> None:
loader = UKOLoader()
ctx.ontology = loader.load(_TTL_PATH)
@then("the ontology should load without errors for uko_l2")
def step_check_ontology_loaded(ctx: Context) -> None:
assert ctx.ontology is not None
assert len(ctx.ontology.nodes) > 0
@then("the ontology should contain uko-func prefix for uko_l2")
def step_check_func_prefix(ctx: Context) -> None:
prefixes = {p.prefix for p in ctx.ontology.prefixes}
assert "uko-func" in prefixes, f"uko-func not in {prefixes}"
@then("the ontology should contain uko-proc prefix for uko_l2")
def step_check_proc_prefix(ctx: Context) -> None:
prefixes = {p.prefix for p in ctx.ontology.prefixes}
assert "uko-proc" in prefixes, f"uko-proc not in {prefixes}"
@then('the ontology should contain node "{label}" for uko_l2')
def step_check_node_exists(ctx: Context, label: str) -> None:
labels = {n.label for n in ctx.ontology.nodes}
assert label in labels, f"Node '{label}' not in {labels}"
@then("the ontology should have at least {count:d} layer 2 nodes for uko_l2")
def step_check_layer2_count(ctx: Context, count: int) -> None:
layer2 = [n for n in ctx.ontology.nodes if n.layer == 2]
assert len(layer2) >= count, f"Expected >= {count} layer 2 nodes, got {len(layer2)}"
# ---------------------------------------------------------------------------
# ParadigmVocabulary frozen
# ---------------------------------------------------------------------------
@when("I try to mutate the vocabulary prefix for uko_l2")
def step_mutate_vocab_prefix(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
ctx.vocabulary.prefix = "changed:" # type: ignore[misc] # deliberate mutation to test frozen model
# ---------------------------------------------------------------------------
# Additional coverage steps
# ---------------------------------------------------------------------------
@when("I create a paradigm vocabulary with empty prefix for uko_l2")
def step_create_vocab_empty_prefix(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
ParadigmVocabulary(
prefix="",
iri="http://test",
layer=2,
parent_domain="uko-code:",
max_depth=0,
)
@when("I create a paradigm vocabulary with whitespace prefix for uko_l2")
def step_create_vocab_whitespace_prefix(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
ParadigmVocabulary(
prefix=" ",
iri="http://test",
layer=2,
parent_domain="uko-code:",
max_depth=0,
)
@when("I create a vocabulary property with whitespace uri for uko_l2")
def step_create_vocab_prop_whitespace_uri(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri=" ",
label="test",
domain_uri="https://example.com/d",
range_uri="https://example.com/r",
)
@then("the registry should return False for non-string contains for uko_l2")
def step_check_non_string_contains(ctx: Context) -> None:
assert 42 not in ctx.registry
assert None not in ctx.registry
@when("I try to register a vocabulary with duplicate IRI for uko_l2")
def step_register_duplicate_iri(ctx: Context) -> None:
oo = get_oo_vocabulary()
dup = ParadigmVocabulary(
prefix="uko-dup:",
iri=oo.iri,
layer=2,
parent_domain="uko-code:",
)
with capture_error(ctx, ValueError):
ctx.registry.register(dup)
@when("I create a paradigm vocabulary with prefix missing colon for uko_l2")
def step_create_paradigm_vocab_no_colon(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
ParadigmVocabulary(
prefix="uko-test",
iri="https://example.org/test#",
layer=2,
parent_domain="uko-code:",
)
+386
View File
@@ -0,0 +1,386 @@
"""Behave steps for UKO Layer 2 detail-level map tests.
Covers DetailLevelMap base maps, DetailLevelMapBuilder,
``build_effective_map``, and unknown-level resolution.
All step definitions use the ``for uko_l2`` suffix to avoid
AmbiguousStep collisions with other feature files.
"""
from __future__ import annotations
import copy
from types import MappingProxyType
from _uko_l2_test_helpers import capture_error
from behave import given, then, when
from behave.runner import Context
from cleveragents.acms.uko.detail_level_maps import (
CODE_DETAIL_LEVEL_MAP,
FUNC_DETAIL_LEVEL_MAP,
OO_DETAIL_LEVEL_MAP,
PROC_DETAIL_LEVEL_MAP,
DetailLevelMapBuilder,
build_effective_map,
)
from cleveragents.domain.models.acms.crp import (
DetailLevelCycleError,
DetailLevelMap,
)
# ---------------------------------------------------------------------------
# DetailLevelMap -- base maps
# ---------------------------------------------------------------------------
@given("the uko-code detail level map for uko_l2")
def step_get_code_map(ctx: Context) -> None:
ctx.detail_map = CODE_DETAIL_LEVEL_MAP
@given("the uko-oo detail level map for uko_l2")
def step_get_oo_map(ctx: Context) -> None:
ctx.detail_map = OO_DETAIL_LEVEL_MAP
@given("the uko-func detail level map for uko_l2")
def step_get_func_map(ctx: Context) -> None:
ctx.detail_map = FUNC_DETAIL_LEVEL_MAP
@given("the uko-proc detail level map for uko_l2")
def step_get_proc_map(ctx: Context) -> None:
ctx.detail_map = PROC_DETAIL_LEVEL_MAP
@then('the map should have domain "{expected}" for uko_l2')
def step_check_map_domain(ctx: Context, expected: str) -> None:
assert ctx.detail_map.domain == expected
@then("the map should have max_depth {expected:d} for uko_l2")
def step_check_map_max_depth(ctx: Context, expected: int) -> None:
assert ctx.detail_map.max_depth == expected
@then('the map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_map_resolve_name(ctx: Context, name: str, expected: int) -> None:
assert ctx.detail_map.resolve(name) == expected
@then("the map should have {count:d} levels for uko_l2")
def step_check_map_level_count(ctx: Context, count: int) -> None:
assert len(ctx.detail_map.effective_levels()) == count
@then("the map should resolve integer {value:d} to {expected:d} for uko_l2")
def step_check_map_resolve_int(ctx: Context, value: int, expected: int) -> None:
assert ctx.detail_map.resolve(value) == expected
@then('the map parent domain should be "{expected}" for uko_l2')
def step_check_map_parent_domain(ctx: Context, expected: str) -> None:
assert ctx.detail_map.parent is not None
assert ctx.detail_map.parent.domain == expected
# ---------------------------------------------------------------------------
# DetailLevelMapBuilder
# ---------------------------------------------------------------------------
@given(
'a detail level map builder with parent "{parent}" and domain "{domain}" for uko_l2'
)
def step_create_builder(ctx: Context, parent: str, domain: str) -> None:
parent_map = CODE_DETAIL_LEVEL_MAP if parent == "uko-code:" else None
assert parent_map is not None, f"Unknown parent: {parent}"
ctx.builder = DetailLevelMapBuilder(parent_map, domain)
@when('I insert "{name}" after "{after}" for uko_l2')
def step_builder_insert(ctx: Context, name: str, after: str) -> None:
ctx.builder.insert_after(after, name)
@when("I build the map for uko_l2")
def step_builder_build(ctx: Context) -> None:
ctx.built_map = ctx.builder.build()
@then('the built map should have domain "{expected}" for uko_l2')
def step_check_built_domain(ctx: Context, expected: str) -> None:
assert ctx.built_map.domain == expected
@then('the built map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_built_resolve(ctx: Context, name: str, expected: int) -> None:
assert ctx.built_map.resolve(name) == expected
@then("the built map should have {count:d} levels for uko_l2")
def step_check_built_levels(ctx: Context, count: int) -> None:
assert len(ctx.built_map.effective_levels()) == count
@when('I try to insert after non-existent level "{name}" for uko_l2')
def step_insert_nonexistent(ctx: Context, name: str) -> None:
with capture_error(ctx, ValueError):
ctx.builder.insert_after(name, "TEST")
@then("a value error should be raised for uko_l2")
def step_check_value_error(ctx: Context) -> None:
assert ctx.error is not None
assert isinstance(ctx.error, ValueError)
@when("I try to create a builder with empty domain for uko_l2")
def step_create_builder_empty_domain(ctx: Context) -> None:
with capture_error(ctx, ValueError):
DetailLevelMapBuilder(CODE_DETAIL_LEVEL_MAP, "")
@when('I try to insert empty level name after "{after}" for uko_l2')
def step_insert_empty_name(ctx: Context, after: str) -> None:
with capture_error(ctx, ValueError):
ctx.builder.insert_after(after, "")
@when('I try to insert "{name}" after empty level name for uko_l2')
def step_insert_after_empty(ctx: Context, name: str) -> None:
with capture_error(ctx, ValueError):
ctx.builder.insert_after("", name)
@then('the builder parent domain should be "{expected}" for uko_l2')
def step_check_builder_parent(ctx: Context, expected: str) -> None:
assert ctx.builder.parent.domain == expected
@then('the builder domain should be "{expected}" for uko_l2')
def step_check_builder_domain(ctx: Context, expected: str) -> None:
assert ctx.builder.domain == expected
# ---------------------------------------------------------------------------
# build_effective_map function
# ---------------------------------------------------------------------------
@when("I build an effective map with no insertions for uko_l2")
def step_build_effective_no_insert(ctx: Context) -> None:
ctx.effective = build_effective_map(ctx.detail_map, [])
@when('I build an effective map with insertion "{name}" after "{after}" for uko_l2')
def step_build_effective_insert(ctx: Context, name: str, after: str) -> None:
ctx.effective = build_effective_map(ctx.detail_map, [(after, name)])
@when('I try to build an effective map with insertion after "{after}" for uko_l2')
def step_build_effective_invalid(ctx: Context, after: str) -> None:
with capture_error(ctx, ValueError):
build_effective_map(ctx.detail_map, [(after, "TEST")])
@then("the effective map should have {count:d} entries for uko_l2")
def step_check_effective_count(ctx: Context, count: int) -> None:
assert len(ctx.effective) == count
@then(
'the effective map entry {index:d} should be "{name}" at depth {depth:d} for uko_l2'
)
def step_check_effective_entry(ctx: Context, index: int, name: str, depth: int) -> None:
entry_name, entry_depth = ctx.effective[index]
assert entry_name == name, f"Expected '{name}' but got '{entry_name}'"
assert entry_depth == depth, f"Expected depth {depth} but got {entry_depth}"
# ---------------------------------------------------------------------------
# Unknown level resolution
# ---------------------------------------------------------------------------
@when('I try to resolve unknown level "{name}" for uko_l2')
def step_resolve_unknown(ctx: Context, name: str) -> None:
with capture_error(ctx, ValueError):
ctx.detail_map.resolve(name)
# ---------------------------------------------------------------------------
# Fresh DetailLevelMap (register / mutate)
# ---------------------------------------------------------------------------
@given("a fresh detail level map for uko_l2")
def step_create_fresh_map(ctx: Context) -> None:
ctx.fresh_map = DetailLevelMap(
domain="test:", parent=None, levels={"BASE": 0}, max_depth=10
)
@when('I register level "{name}" with value {value:d} for uko_l2')
def step_register_level(ctx: Context, name: str, value: int) -> None:
ctx.fresh_map.register(name, value)
@then('the fresh map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_fresh_resolve(ctx: Context, name: str, expected: int) -> None:
assert ctx.fresh_map.resolve(name) == expected
@when('I try to register level "{name}" with value {value:d} for uko_l2')
def step_try_register_negative(ctx: Context, name: str, value: int) -> None:
with capture_error(ctx, ValueError):
ctx.fresh_map.register(name, value)
@when("I try to mutate the levels dict for uko_l2")
def step_mutate_levels(ctx: Context) -> None:
with capture_error(ctx, TypeError):
ctx.detail_map.levels["HACK"] = 99
@then("a type error should be raised for uko_l2")
def step_check_type_error(ctx: Context) -> None:
assert ctx.error is not None
assert isinstance(ctx.error, TypeError)
@when('I try to insert duplicate level "{name}" after "{after}" for uko_l2')
def step_try_insert_duplicate(ctx: Context, name: str, after: str) -> None:
with capture_error(ctx, ValueError):
ctx.builder.insert_after(after, name)
@when("I try to build an effective map with duplicate level for uko_l2")
def step_build_effective_duplicate(ctx: Context) -> None:
with capture_error(ctx, ValueError):
build_effective_map(ctx.detail_map, [("MEMBER_LISTING", "MODULE_LISTING")])
@when("I try to mutate effective_levels for uko_l2")
def step_mutate_effective_levels(ctx: Context) -> None:
with capture_error(ctx, TypeError):
effective = ctx.detail_map.effective_levels()
effective["HACK"] = 99 # type: ignore[index] # deliberate mutation test
# ---------------------------------------------------------------------------
# Review findings: H3 — MappingProxyType serialization
# ---------------------------------------------------------------------------
@when("I serialize the detail level map to JSON for uko_l2")
def step_serialize_map_json(ctx: Context) -> None:
ctx.serialized_json = ctx.detail_map.model_dump_json()
@then('the serialized JSON should contain "{expected}" for uko_l2')
def step_check_serialized_json(ctx: Context, expected: str) -> None:
assert expected in ctx.serialized_json
@when("I dump the detail level map to dict for uko_l2")
def step_dump_map_dict(ctx: Context) -> None:
ctx.dumped_dict = ctx.detail_map.model_dump()
@then('the dumped dict levels should contain "{expected}" for uko_l2')
def step_check_dumped_dict(ctx: Context, expected: str) -> None:
assert expected in ctx.dumped_dict["levels"]
# ---------------------------------------------------------------------------
# Review findings: M5 — cycle guard in resolve()
# ---------------------------------------------------------------------------
@given("a detail level map with circular parent for uko_l2")
def step_create_circular_map(ctx: Context) -> None:
# Build two maps that point to each other
map_a = DetailLevelMap(domain="cycle-a:", parent=None, levels={"A": 0}, max_depth=5)
map_b = DetailLevelMap(
domain="cycle-b:", parent=map_a, levels={"B": 1}, max_depth=5
)
# Introduce cycle: map_a.parent -> map_b
object.__setattr__(map_a, "parent", map_b)
ctx.circular_map = map_a
@when('I try to resolve level "{name}" on the circular map for uko_l2')
def step_resolve_circular(ctx: Context, name: str) -> None:
with capture_error(ctx, DetailLevelCycleError):
ctx.circular_map.resolve(name)
@then("a cycle error should be raised for uko_l2")
def step_check_cycle_error(ctx: Context) -> None:
assert ctx.error is not None
assert isinstance(ctx.error, DetailLevelCycleError)
# ---------------------------------------------------------------------------
# Review findings: H2 — levels re-frozen on assignment
# ---------------------------------------------------------------------------
@when("I assign a dict to the levels field for uko_l2")
def step_assign_dict_levels(ctx: Context) -> None:
ctx.fresh_map.levels = {"NEW_LEVEL": 3} # type: ignore[assignment] # deliberate test
ctx.levels_type = type(ctx.fresh_map.levels)
ctx.is_mapping_proxy = isinstance(ctx.fresh_map.levels, MappingProxyType)
@then("the levels should still be an immutable MappingProxy for uko_l2")
def step_check_levels_refrozen(ctx: Context) -> None:
assert ctx.is_mapping_proxy, f"Expected MappingProxyType but got {ctx.levels_type}"
# Also verify it's actually immutable.
# Note: capture_error() is not used here because the pattern is
# "assert that an exception IS raised" (inline assertion), not
# "capture the error for a later @then step".
try:
ctx.fresh_map.levels["HACK"] = 99 # type: ignore[index] # deliberate mutation test
msg = "Expected TypeError on mutation"
raise AssertionError(msg)
except TypeError:
pass
# ---------------------------------------------------------------------------
# Review findings: F7 — cycle guard in effective_levels()
# ---------------------------------------------------------------------------
@when("I try to get effective levels on the circular map for uko_l2")
def step_effective_levels_circular(ctx: Context) -> None:
with capture_error(ctx, DetailLevelCycleError):
ctx.circular_map.effective_levels()
# ---------------------------------------------------------------------------
# Review findings: F13 — deepcopy support
# ---------------------------------------------------------------------------
@when("I deep copy the detail level map for uko_l2")
def step_deep_copy_map(ctx: Context) -> None:
ctx.copied_map = copy.deepcopy(ctx.detail_map)
@then('the copied map should have domain "{expected}" for uko_l2')
def step_check_copied_domain(ctx: Context, expected: str) -> None:
assert ctx.copied_map.domain == expected
@then('the copied map should resolve "{name}" to {expected:d} for uko_l2')
def step_check_copied_resolve(ctx: Context, name: str, expected: int) -> None:
assert ctx.copied_map.resolve(name) == expected
@then("the copied map should not be the same object for uko_l2")
def step_check_copied_identity(ctx: Context) -> None:
assert ctx.copied_map is not ctx.detail_map
@@ -0,0 +1,123 @@
"""Behave steps for UKO Layer 2 vocabulary registry tests.
All step definitions use the ``for uko_l2`` suffix to avoid
AmbiguousStep collisions with other feature files.
"""
from __future__ import annotations
from _uko_l2_test_helpers import capture_error
from behave import given, then, when
from behave.runner import Context
from cleveragents.acms.uko.vocabularies import (
get_func_vocabulary,
get_oo_vocabulary,
get_proc_vocabulary,
)
from cleveragents.acms.uko.vocabulary_registry import VocabularyRegistry
# ---------------------------------------------------------------------------
# VocabularyRegistry -- construction and lookup
# ---------------------------------------------------------------------------
@given("a vocabulary registry with all paradigm vocabularies for uko_l2")
def step_create_registry(ctx: Context) -> None:
ctx.registry = VocabularyRegistry(
vocabularies=(
get_oo_vocabulary(),
get_func_vocabulary(),
get_proc_vocabulary(),
)
)
@then("the registry should have {count:d} vocabularies for uko_l2")
def step_check_registry_count(ctx: Context, count: int) -> None:
assert len(ctx.registry) == count
@then('the registry should contain prefix "{prefix}" for uko_l2')
def step_check_registry_prefix(ctx: Context, prefix: str) -> None:
assert ctx.registry.get_by_prefix(prefix) is not None
@when('I look up vocabulary by prefix "{prefix}" for uko_l2')
def step_lookup_vocab_prefix(ctx: Context, prefix: str) -> None:
ctx.looked_up_vocab = ctx.registry.get_by_prefix(prefix)
@when('I look up vocabulary by IRI "{iri}" for uko_l2')
def step_lookup_vocab_iri(ctx: Context, iri: str) -> None:
ctx.looked_up_vocab = ctx.registry.get_by_iri(iri)
@then("the looked up vocabulary should not be None for uko_l2")
def step_vocab_not_none(ctx: Context) -> None:
assert ctx.looked_up_vocab is not None
@then("the looked up vocabulary should be None for uko_l2")
def step_vocab_is_none(ctx: Context) -> None:
assert ctx.looked_up_vocab is None
@then('the looked up vocabulary prefix should be "{expected}" for uko_l2')
def step_check_looked_up_vocab_prefix(ctx: Context, expected: str) -> None:
assert ctx.looked_up_vocab is not None
assert ctx.looked_up_vocab.prefix == expected
# ---------------------------------------------------------------------------
# Registration / deregistration
# ---------------------------------------------------------------------------
@when("I try to register a duplicate vocabulary for uko_l2")
def step_register_duplicate(ctx: Context) -> None:
with capture_error(ctx, ValueError):
ctx.registry.register(get_oo_vocabulary())
@then("a duplicate registration error should be raised for uko_l2")
def step_check_duplicate_error(ctx: Context) -> None:
assert ctx.error is not None
assert isinstance(ctx.error, ValueError)
assert "already registered" in str(ctx.error)
@when('I unregister prefix "{prefix}" from the registry for uko_l2')
def step_unregister_prefix(ctx: Context, prefix: str) -> None:
ctx.unregister_result = ctx.registry.unregister(prefix)
@then("the unregister result should be False for uko_l2")
def step_check_unregister_false(ctx: Context) -> None:
assert ctx.unregister_result is False
# ---------------------------------------------------------------------------
# Enumeration and contains
# ---------------------------------------------------------------------------
@then('list_prefixes should return "{expected}" for uko_l2')
def step_check_list_prefixes(ctx: Context, expected: str) -> None:
prefixes = ctx.registry.list_prefixes()
assert ",".join(prefixes) == expected
@then("list_all should return {count:d} vocabularies for uko_l2")
def step_check_list_all(ctx: Context, count: int) -> None:
assert len(ctx.registry.list_all()) == count
@then('"{prefix}" should be in the registry for uko_l2')
def step_check_contains(ctx: Context, prefix: str) -> None:
assert prefix in ctx.registry
@then('"{prefix}" should not be in the registry for uko_l2')
def step_check_not_contains(ctx: Context, prefix: str) -> None:
assert prefix not in ctx.registry
+427
View File
@@ -0,0 +1,427 @@
"""Behave steps for UKO Layer 2 vocabulary model and registry tests.
All step definitions use the ``for uko_l2`` suffix to avoid
AmbiguousStep collisions with other feature files.
"""
from __future__ import annotations
from _uko_l2_test_helpers import capture_error
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.acms.uko.vocabularies import (
VocabularyClass,
VocabularyProperty,
get_func_vocabulary,
get_oo_vocabulary,
get_proc_vocabulary,
)
# ---------------------------------------------------------------------------
# VocabularyClass -- construction
# ---------------------------------------------------------------------------
@given('a simple vocabulary class with uri "{uri}" and label "{label}" for uko_l2')
def step_create_vocab_class(ctx: Context, uri: str, label: str) -> None:
ctx.vocab_class = VocabularyClass(uri=uri, label=label)
@given(
'a vocabulary class with uri "{uri}" and label "{label}" '
'and parents "{parents}" for uko_l2'
)
def step_create_vocab_class_parents(
ctx: Context, uri: str, label: str, parents: str
) -> None:
parent_uris = tuple(p.strip() for p in parents.split(","))
ctx.vocab_class = VocabularyClass(uri=uri, label=label, parent_uris=parent_uris)
@then('the vocabulary class uri should be "{expected}" for uko_l2')
def step_check_vocab_class_uri(ctx: Context, expected: str) -> None:
assert ctx.vocab_class.uri == expected
@then('the vocabulary class label should be "{expected}" for uko_l2')
def step_check_vocab_class_label(ctx: Context, expected: str) -> None:
assert ctx.vocab_class.label == expected
@then("the vocabulary class comment should be empty for uko_l2")
def step_check_vocab_class_comment_empty(ctx: Context) -> None:
assert ctx.vocab_class.comment == ""
@then("the vocabulary class parent_uris should be empty for uko_l2")
def step_check_vocab_class_parents_empty(ctx: Context) -> None:
assert ctx.vocab_class.parent_uris == ()
@then("the vocabulary class should have {count:d} parent URIs for uko_l2")
def step_check_vocab_class_parent_count(ctx: Context, count: int) -> None:
assert len(ctx.vocab_class.parent_uris) == count
@then('the vocabulary class parent {index:d} should be "{expected}" for uko_l2')
def step_check_vocab_class_parent_idx(ctx: Context, index: int, expected: str) -> None:
assert ctx.vocab_class.parent_uris[index] == expected
# ---------------------------------------------------------------------------
# VocabularyClass -- validation
# ---------------------------------------------------------------------------
@when("I create a vocabulary class with empty uri for uko_l2")
def step_create_vocab_class_empty_uri(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyClass(uri="", label="Test")
@when("I create a vocabulary class with whitespace uri for uko_l2")
def step_create_vocab_class_whitespace_uri(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyClass(uri=" ", label="Test")
@when("I create a vocabulary class with empty label for uko_l2")
def step_create_vocab_class_empty_label(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyClass(uri="http://example.org/test", label="")
@then("a validation error should be raised for uko_l2")
def step_check_validation_error(ctx: Context) -> None:
assert ctx.error is not None, "Expected a validation error but none was raised"
assert isinstance(ctx.error, (ValidationError, ValueError))
# ---------------------------------------------------------------------------
# VocabularyClass -- frozen
# ---------------------------------------------------------------------------
@when("I try to mutate the vocabulary class uri for uko_l2")
def step_mutate_vocab_class(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
ctx.vocab_class.uri = "http://changed" # type: ignore[misc] # deliberate mutation to test frozen model
@then("a frozen mutation error should be raised for uko_l2")
def step_check_frozen_error(ctx: Context) -> None:
assert ctx.error is not None, "Expected a frozen mutation error"
# ---------------------------------------------------------------------------
# VocabularyProperty -- construction
# ---------------------------------------------------------------------------
@given(
'a basic vocabulary property with uri "{uri}" and label "{label}" '
'and domain "{domain}" and range "{range_uri}" for uko_l2'
)
def step_create_vocab_property(
ctx: Context, uri: str, label: str, domain: str, range_uri: str
) -> None:
ctx.vocab_property = VocabularyProperty(
uri=uri, label=label, domain_uri=domain, range_uri=range_uri
)
@given(
'a vocabulary property with uri "{uri}" and label "{label}" '
'and domain "{domain}" and range "{range_uri}" '
'and sub_property_of "{sub_prop}" for uko_l2'
)
def step_create_vocab_property_sub(
ctx: Context,
uri: str,
label: str,
domain: str,
range_uri: str,
sub_prop: str,
) -> None:
ctx.vocab_property = VocabularyProperty(
uri=uri,
label=label,
domain_uri=domain,
range_uri=range_uri,
sub_property_of=sub_prop,
)
@then('the vocabulary property uri should be "{expected}" for uko_l2')
def step_check_vocab_prop_uri(ctx: Context, expected: str) -> None:
assert ctx.vocab_property.uri == expected
@then('the vocabulary property label should be "{expected}" for uko_l2')
def step_check_vocab_prop_label(ctx: Context, expected: str) -> None:
assert ctx.vocab_property.label == expected
@then("the vocabulary property sub_property_of should be empty for uko_l2")
def step_check_vocab_prop_sub_empty(ctx: Context) -> None:
assert ctx.vocab_property.sub_property_of is None
@then('the vocabulary property sub_property_of should be "{expected}" for uko_l2')
def step_check_vocab_prop_sub(ctx: Context, expected: str) -> None:
assert ctx.vocab_property.sub_property_of == expected
@when("I create a vocabulary property with empty uri for uko_l2")
def step_create_vocab_prop_empty_uri(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="",
label="test",
domain_uri="https://example.com/d",
range_uri="https://example.com/r",
)
@when("I try to mutate the vocabulary property uri for uko_l2")
def step_mutate_vocab_property(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
ctx.vocab_property.uri = "http://changed" # type: ignore[misc] # deliberate mutation to test frozen model
# ---------------------------------------------------------------------------
# Paradigm vocabularies -- OO, Func, Proc
# ---------------------------------------------------------------------------
@given("the uko-oo vocabulary for uko_l2")
def step_get_oo_vocab(ctx: Context) -> None:
ctx.vocabulary = get_oo_vocabulary()
@given("the uko-func vocabulary for uko_l2")
def step_get_func_vocab(ctx: Context) -> None:
ctx.vocabulary = get_func_vocabulary()
@given("the uko-proc vocabulary for uko_l2")
def step_get_proc_vocab(ctx: Context) -> None:
ctx.vocabulary = get_proc_vocabulary()
@then('the vocabulary should have prefix "{expected}" for uko_l2')
def step_check_vocab_prefix(ctx: Context, expected: str) -> None:
assert ctx.vocabulary.prefix == expected
@then('the vocabulary should have IRI "{expected}" for uko_l2')
def step_check_vocab_iri(ctx: Context, expected: str) -> None:
assert ctx.vocabulary.iri == expected
@then("the vocabulary should have layer {layer:d} for uko_l2")
def step_check_vocab_layer(ctx: Context, layer: int) -> None:
assert ctx.vocabulary.layer == layer
@then("the vocabulary should have {count:d} classes for uko_l2")
def step_check_vocab_class_count(ctx: Context, count: int) -> None:
assert len(ctx.vocabulary.classes) == count
@then("the vocabulary should have {count:d} properties for uko_l2")
def step_check_vocab_prop_count(ctx: Context, count: int) -> None:
assert len(ctx.vocabulary.properties) == count
@then('the vocabulary should contain class "{name}" for uko_l2')
def step_check_vocab_has_class(ctx: Context, name: str) -> None:
found = ctx.vocabulary.get_class(name)
assert found is not None, f"Class '{name}' not found"
@then('the vocabulary should contain property "{name}" for uko_l2')
def step_check_vocab_has_property(ctx: Context, name: str) -> None:
found = ctx.vocabulary.get_property(name)
assert found is not None, f"Property '{name}' not found"
@when('I look up class "{name}" for uko_l2')
def step_lookup_class(ctx: Context, name: str) -> None:
ctx.looked_up_class = ctx.vocabulary.get_class(name)
@when('I look up property "{name}" for uko_l2')
def step_lookup_property(ctx: Context, name: str) -> None:
ctx.looked_up_property = ctx.vocabulary.get_property(name)
@then('the class should have parent "{expected}" for uko_l2')
def step_check_class_parent(ctx: Context, expected: str) -> None:
assert ctx.looked_up_class is not None, "Class was not looked up"
assert expected in ctx.looked_up_class.parent_uris, (
f"Expected parent '{expected}' not found in {ctx.looked_up_class.parent_uris}"
)
@then('the property domain should be "{expected}" for uko_l2')
def step_check_prop_domain(ctx: Context, expected: str) -> None:
assert ctx.looked_up_property is not None
assert ctx.looked_up_property.domain_uri == expected
@then('the property range should be "{expected}" for uko_l2')
def step_check_prop_range(ctx: Context, expected: str) -> None:
assert ctx.looked_up_property is not None
assert ctx.looked_up_property.range_uri == expected
@then('the property sub_property_of should be "{expected}" for uko_l2')
def step_check_prop_sub_property(ctx: Context, expected: str) -> None:
assert ctx.looked_up_property is not None
assert ctx.looked_up_property.sub_property_of == expected
@then("the looked up class should not be None for uko_l2")
def step_class_not_none(ctx: Context) -> None:
assert ctx.looked_up_class is not None
@then("the looked up class should be None for uko_l2")
def step_class_is_none(ctx: Context) -> None:
assert ctx.looked_up_class is None
@then('the looked up class uri should be "{expected}" for uko_l2')
def step_check_looked_up_class_uri(ctx: Context, expected: str) -> None:
assert ctx.looked_up_class is not None
assert ctx.looked_up_class.uri == expected
@then("the looked up property should not be None for uko_l2")
def step_prop_not_none(ctx: Context) -> None:
assert ctx.looked_up_property is not None
@then("the looked up property should be None for uko_l2")
def step_prop_is_none(ctx: Context) -> None:
assert ctx.looked_up_property is None
@then("class_uris should have {count:d} entries for uko_l2")
def step_check_class_uris_count(ctx: Context, count: int) -> None:
assert len(ctx.vocabulary.class_uris()) == count
@then("property_uris should have {count:d} entries for uko_l2")
def step_check_property_uris_count(ctx: Context, count: int) -> None:
assert len(ctx.vocabulary.property_uris()) == count
# ---------------------------------------------------------------------------
# Review findings: M8 — empty label/domain/range rejection
# ---------------------------------------------------------------------------
@when("I create a vocabulary property with empty label for uko_l2")
def step_create_vocab_prop_empty_label(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="https://example.com/prop",
label="",
domain_uri="https://example.com/d",
range_uri="https://example.com/r",
)
@when("I create a vocabulary property with empty domain for uko_l2")
def step_create_vocab_prop_empty_domain(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="https://example.com/prop",
label="test",
domain_uri="",
range_uri="https://example.com/r",
)
@when("I create a vocabulary property with empty range for uko_l2")
def step_create_vocab_prop_empty_range(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="https://example.com/prop",
label="test",
domain_uri="https://example.com/d",
range_uri="",
)
# ---------------------------------------------------------------------------
# Review findings: M9 — invalid URI scheme rejection
# ---------------------------------------------------------------------------
@when("I create a vocabulary class with non-http uri for uko_l2")
def step_create_vocab_class_non_http(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyClass(
uri="ftp://example.com/BadClass",
label="BadClass",
)
@when("I create a vocabulary property with non-http uri for uko_l2")
def step_create_vocab_prop_non_http(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="urn:example:bad",
label="bad",
domain_uri="https://example.com/d",
range_uri="https://example.com/r",
)
@when("I create a vocabulary class with non-http parent uri for uko_l2")
def step_create_vocab_class_non_http_parent(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyClass(
uri="https://example.com/Good",
label="Good",
parent_uris=("ftp://example.com/BadParent",),
)
@when("I create a vocabulary property with non-http domain uri for uko_l2")
def step_create_vocab_prop_non_http_domain(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="https://example.com/prop",
label="prop",
domain_uri="ftp://example.com/BadDomain",
range_uri="https://example.com/r",
)
@when("I create a vocabulary property with non-http range uri for uko_l2")
def step_create_vocab_prop_non_http_range(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="https://example.com/prop",
label="prop",
domain_uri="https://example.com/d",
range_uri="file:///etc/passwd",
)
@when("I create a vocabulary property with non-http sub_property_of for uko_l2")
def step_create_vocab_prop_non_http_subprop(ctx: Context) -> None:
with capture_error(ctx, ValidationError):
VocabularyProperty(
uri="https://example.com/prop",
label="prop",
domain_uri="https://example.com/d",
range_uri="https://example.com/r",
sub_property_of="javascript:alert(1)",
)
+1 -1
View File
@@ -189,7 +189,7 @@ Feature: UKO Ontology Loader
| layer | count |
| 0 | 18 |
| 1 | 67 |
| 2 | 6 |
| 2 | 14 |
# ---------------------------------------------------------------------------
# rdfs:domain and rdfs:range URI resolution
+239
View File
@@ -0,0 +1,239 @@
"""Robot Framework helper for UKO Layer 2 paradigm vocabulary tests.
Subcommands:
vocab-oo -- verify uko-oo: vocabulary, print ``vocab-oo-ok``
vocab-func -- verify uko-func: vocabulary, print ``vocab-func-ok``
vocab-proc -- verify uko-proc: vocabulary, print ``vocab-proc-ok``
detail-map-chain -- verify L2->L1->L0 chain, print ``detail-map-chain-ok``
ttl-validate -- validate TTL with new prefixes, print ``ttl-validate-ok``
"""
from __future__ import annotations
import sys
import traceback
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.acms.uko.detail_level_maps import ( # noqa: E402
CODE_DETAIL_LEVEL_MAP,
FUNC_DETAIL_LEVEL_MAP,
OO_DETAIL_LEVEL_MAP,
PROC_DETAIL_LEVEL_MAP,
)
from cleveragents.acms.uko.vocabularies import ( # noqa: E402
get_func_vocabulary,
get_oo_vocabulary,
get_proc_vocabulary,
)
from cleveragents.acms.uko.vocabulary_registry import VocabularyRegistry # noqa: E402
from cleveragents.application.services.uko_loader import UKOLoader # noqa: E402
_TTL_PATH = Path(__file__).resolve().parents[1] / "docs" / "ontology" / "uko.ttl"
def _cmd_vocab_oo() -> int:
"""Verify uko-oo: vocabulary has expected classes and properties."""
try:
vocab = get_oo_vocabulary()
if len(vocab.classes) != 4:
print(f"vocab-oo-fail: expected 4 classes, got {len(vocab.classes)}")
return 1
if len(vocab.properties) != 2:
print(f"vocab-oo-fail: expected 2 properties, got {len(vocab.properties)}")
return 1
expected_classes = {"Class", "Interface", "Method", "Attribute"}
actual_classes = {c.label for c in vocab.classes}
if actual_classes != expected_classes:
print(f"vocab-oo-fail: classes mismatch: {actual_classes}")
return 1
print(
f"vocab-oo-ok: {len(vocab.classes)} classes, "
f"{len(vocab.properties)} properties"
)
return 0
except Exception as exc: # robot helper must not crash
traceback.print_exc()
print(f"vocab-oo-fail: {exc}")
return 1
def _cmd_vocab_func() -> int:
"""Verify uko-func: vocabulary has expected classes."""
try:
vocab = get_func_vocabulary()
if len(vocab.classes) != 3:
print(f"vocab-func-fail: expected 3 classes, got {len(vocab.classes)}")
return 1
expected = {"PureFunction", "TypeClass", "Monad"}
actual = {c.label for c in vocab.classes}
if actual != expected:
print(f"vocab-func-fail: classes mismatch: {actual}")
return 1
print(f"vocab-func-ok: {len(vocab.classes)} classes")
return 0
except Exception as exc: # robot helper must not crash
traceback.print_exc()
print(f"vocab-func-fail: {exc}")
return 1
def _cmd_vocab_proc() -> int:
"""Verify uko-proc: vocabulary has expected classes."""
try:
vocab = get_proc_vocabulary()
if len(vocab.classes) != 5:
print(f"vocab-proc-fail: expected 5 classes, got {len(vocab.classes)}")
return 1
expected = {
"ProceduralFunction",
"GlobalVariable",
"HeaderFile",
"StructDefinition",
"Macro",
}
actual = {c.label for c in vocab.classes}
if actual != expected:
print(f"vocab-proc-fail: classes mismatch: {actual}")
return 1
print(f"vocab-proc-ok: {len(vocab.classes)} classes")
return 0
except Exception as exc: # robot helper must not crash
traceback.print_exc()
print(f"vocab-proc-fail: {exc}")
return 1
def _cmd_detail_map_chain() -> int:
"""Verify DetailLevelMap inheritance chain: L2 -> L1 -> L0."""
try:
# Verify OO map extends CODE map
oo_map = OO_DETAIL_LEVEL_MAP
if oo_map.parent is None:
print("detail-map-chain-fail: OO map has no parent")
return 1
if oo_map.parent.domain != "uko-code:":
print(f"detail-map-chain-fail: OO parent is {oo_map.parent.domain}")
return 1
# Verify OO map has CLASS_HIERARCHY at depth 3
depth = oo_map.resolve("CLASS_HIERARCHY")
if depth != 3:
print(f"detail-map-chain-fail: CLASS_HIERARCHY at {depth}, expected 3")
return 1
# Verify OO map has VISIBILITY_ANNOTATED at depth 7
depth = oo_map.resolve("VISIBILITY_ANNOTATED")
if depth != 7:
print(f"detail-map-chain-fail: VISIBILITY_ANNOTATED at {depth}, expected 7")
return 1
# Verify FULL_SOURCE shifted to 11
depth = oo_map.resolve("FULL_SOURCE")
if depth != 11:
print(f"detail-map-chain-fail: FULL_SOURCE at {depth}, expected 11")
return 1
# Verify func and proc maps have correct parent
func_map = FUNC_DETAIL_LEVEL_MAP
proc_map = PROC_DETAIL_LEVEL_MAP
if func_map.parent is None or func_map.parent.domain != "uko-code:":
print("detail-map-chain-fail: FUNC map parent incorrect")
return 1
if proc_map.parent is None or proc_map.parent.domain != "uko-code:":
print("detail-map-chain-fail: PROC map parent incorrect")
return 1
# Verify CODE map has no parent
code_map = CODE_DETAIL_LEVEL_MAP
if code_map.parent is not None:
print("detail-map-chain-fail: CODE map should have no parent")
return 1
print(
f"detail-map-chain-ok: OO={len(oo_map.levels)} levels, "
f"FUNC={len(func_map.levels)}, PROC={len(proc_map.levels)}, "
f"CODE={len(code_map.levels)}"
)
return 0
except Exception as exc: # robot helper must not crash
traceback.print_exc()
print(f"detail-map-chain-fail: {exc}")
return 1
def _cmd_ttl_validate() -> int:
"""Load and validate the updated TTL file with new prefixes."""
try:
loader = UKOLoader()
ontology = loader.load(_TTL_PATH)
prefixes = {p.prefix for p in ontology.prefixes}
if "uko-func" not in prefixes:
print("ttl-validate-fail: uko-func prefix missing")
return 1
if "uko-proc" not in prefixes:
print("ttl-validate-fail: uko-proc prefix missing")
return 1
# Check layer 2 node count
layer2 = [n for n in ontology.nodes if n.layer == 2]
if len(layer2) < 12:
print(f"ttl-validate-fail: expected >= 12 L2 nodes, got {len(layer2)}")
return 1
# Verify registry can be populated
registry = VocabularyRegistry(
vocabularies=(
get_oo_vocabulary(),
get_func_vocabulary(),
get_proc_vocabulary(),
)
)
if len(registry) != 3:
print(f"ttl-validate-fail: registry has {len(registry)} vocabs")
return 1
print(
f"ttl-validate-ok: {len(ontology.nodes)} nodes, "
f"{len(layer2)} layer-2, {len(registry)} vocabs"
)
return 0
except Exception as exc: # robot helper must not crash
traceback.print_exc()
print(f"ttl-validate-fail: {exc}")
return 1
_COMMANDS: dict[str, Callable[[], int]] = {
"vocab-oo": _cmd_vocab_oo,
"vocab-func": _cmd_vocab_func,
"vocab-proc": _cmd_vocab_proc,
"detail-map-chain": _cmd_detail_map_chain,
"ttl-validate": _cmd_ttl_validate,
}
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print(
"Usage: helper_uko_layer2_paradigm.py "
"<vocab-oo|vocab-func|vocab-proc|detail-map-chain|ttl-validate>"
)
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())
+31
View File
@@ -0,0 +1,31 @@
"""ACMS (Advanced Context Management System) UKO vocabulary support.
Provides UKO Layer 2 paradigm vocabulary specializations and the
DetailLevelMap inheritance mechanism for resolving named detail levels
across the ontology hierarchy (Layer 2 -> Layer 1 -> Layer 0).
Based on ``docs/specification.md`` ~lines 42333-42422.
"""
from __future__ import annotations
from cleveragents.acms import uko as _uko
from cleveragents.acms.uko import (
CODE_DETAIL_LEVEL_MAP,
FUNC_DETAIL_LEVEL_MAP,
OO_DETAIL_LEVEL_MAP,
PROC_DETAIL_LEVEL_MAP,
DetailLevelMapBuilder,
ParadigmVocabulary,
VocabularyClass,
VocabularyProperty,
VocabularyRegistry,
build_effective_map,
get_func_vocabulary,
get_oo_vocabulary,
get_proc_vocabulary,
)
# Re-export everything published by the ``uko`` sub-package so the two
# ``__all__`` lists stay in sync automatically.
__all__: list[str] = list(_uko.__all__)
+56
View File
@@ -0,0 +1,56 @@
"""UKO Layer 2 paradigm vocabulary specializations.
Provides Object-Oriented (``uko-oo:``), Functional (``uko-func:``), and
Procedural (``uko-proc:``) vocabulary definitions with DetailLevelMap
inheritance that extends the Layer 1 ``uko-code:`` domain.
Namespace prefixes (per specification.md ~line 41900):
| Prefix | IRI | Layer |
|---------------|-------------------------------------------------|-------|
| ``uko-oo:`` | ``https://cleveragents.ai/ontology/uko/oo#`` | 2 |
| ``uko-func:`` | ``https://cleveragents.ai/ontology/uko/func#`` | 2 |
| ``uko-proc:`` | ``https://cleveragents.ai/ontology/uko/proc#`` | 2 |
Based on ``docs/specification.md`` ~lines 42333-42422.
"""
from __future__ import annotations
from cleveragents.acms.uko.detail_level_maps import (
CODE_DETAIL_LEVEL_MAP,
FUNC_DETAIL_LEVEL_MAP,
OO_DETAIL_LEVEL_MAP,
PROC_DETAIL_LEVEL_MAP,
DetailLevelMapBuilder,
build_effective_map,
)
from cleveragents.acms.uko.vocabularies import (
ParadigmVocabulary,
VocabularyClass,
VocabularyProperty,
get_func_vocabulary,
get_oo_vocabulary,
get_proc_vocabulary,
)
from cleveragents.acms.uko.vocabulary_registry import (
DuplicateVocabularyError,
VocabularyRegistry,
)
__all__: list[str] = [
"CODE_DETAIL_LEVEL_MAP",
"FUNC_DETAIL_LEVEL_MAP",
"OO_DETAIL_LEVEL_MAP",
"PROC_DETAIL_LEVEL_MAP",
"DetailLevelMapBuilder",
"DuplicateVocabularyError",
"ParadigmVocabulary",
"VocabularyClass",
"VocabularyProperty",
"VocabularyRegistry",
"build_effective_map",
"get_func_vocabulary",
"get_oo_vocabulary",
"get_proc_vocabulary",
]
@@ -0,0 +1,296 @@
"""DetailLevelMap inheritance for UKO Layer 2 paradigm vocabularies.
Implements the DetailLevelMap inheritance chain where Layer 2 (paradigm)
maps extend the Layer 1 (``uko-code:``) base map by inserting new named
levels at specific integer positions and reassigning subsequent depths.
The inheritance chain is:
Layer 2 (e.g., ``uko-oo:``) -> Layer 1 (``uko-code:``) -> Layer 0 (``uko:``)
When a paradigm map inserts a level (e.g., ``CLASS_HIERARCHY`` at depth 3),
all subsequent depths in the parent map are shifted upward by one to
maintain consecutive integer numbering.
Thread Safety
-------------
Module-level map constants (``CODE_DETAIL_LEVEL_MAP``, etc.) are
initialised once at import time under the GIL and are thereafter
read-only. Do **not** call ``register()`` on these constants after
module initialisation.
Based on ``docs/specification.md`` ~lines 24923-25027, 42380-42469.
"""
from __future__ import annotations
from types import MappingProxyType
import structlog
from cleveragents.domain.models.acms.crp import DetailLevelMap
__all__: list[str] = [
"CODE_DETAIL_LEVEL_MAP",
"FUNC_DETAIL_LEVEL_MAP",
"OO_DETAIL_LEVEL_MAP",
"PROC_DETAIL_LEVEL_MAP",
"DetailLevelMapBuilder",
"build_effective_map",
]
_log = structlog.get_logger(__name__)
#: Default max depth for code-domain maps (spec lines 24970-24985).
_CODE_MAX_DEPTH: int = 9
#: Parent domain prefix for Layer 2 maps.
_CODE_DOMAIN: str = "uko-code:"
# ---------------------------------------------------------------------------
# Layer 1 base map: uko-code: (spec lines 24970-24985)
# ---------------------------------------------------------------------------
_CODE_LEVELS: MappingProxyType[str, int] = MappingProxyType(
{
"MODULE_LISTING": 0,
"MODULE_GRAPH": 1,
"MEMBER_LISTING": 2,
"MEMBER_SUMMARY": 3,
"SIGNATURES": 4,
"SIGNATURES_WITH_DOCS": 5,
"STRUCTURAL_OUTLINE": 6,
"KEY_LOGIC": 7,
"NEAR_COMPLETE": 8,
"FULL_SOURCE": 9,
}
)
#: Layer 1 base map. Read-only after module initialisation.
CODE_DETAIL_LEVEL_MAP = DetailLevelMap(
domain=_CODE_DOMAIN,
parent=None,
levels=dict(_CODE_LEVELS),
max_depth=_CODE_MAX_DEPTH,
)
# ---------------------------------------------------------------------------
# DetailLevelMap builder with insertion and reassignment logic
# ---------------------------------------------------------------------------
class DetailLevelMapBuilder:
"""Build a child DetailLevelMap that inherits from a parent.
Supports inserting new named levels at specific positions, which
shifts all subsequent integer depth assignments upward.
Args:
parent: The parent DetailLevelMap to inherit from.
domain: The UKO namespace for the new map (e.g., ``"uko-oo:"``).
Raises:
ValueError: If *domain* is empty or whitespace-only.
Example::
builder = DetailLevelMapBuilder(CODE_DETAIL_LEVEL_MAP, "uko-oo:")
builder.insert_after("MEMBER_LISTING", "CLASS_HIERARCHY")
builder.insert_after("SIGNATURES_WITH_DOCS", "VISIBILITY_ANNOTATED")
oo_map = builder.build()
assert oo_map.resolve("CLASS_HIERARCHY") == 3
"""
def __init__(self, parent: DetailLevelMap, domain: str) -> None:
if not domain or not domain.strip():
raise ValueError("domain must be a non-empty string")
self._parent = parent
self._domain = domain.strip()
self._insertions: list[tuple[str, str]] = []
_log.debug(
"detail_level_map_builder.created",
parent_domain=parent.domain,
child_domain=self._domain,
)
@property
def parent(self) -> DetailLevelMap:
"""Return the parent map."""
return self._parent
@property
def domain(self) -> str:
"""Return the domain namespace."""
return self._domain
def insert_after(self, after_level: str, new_level: str) -> None:
"""Insert a new named level after an existing one.
The new level will be assigned the next integer after
*after_level*, and all subsequent levels shift upward by one.
Args:
after_level: Name of the existing level to insert after.
new_level: Name of the new level to insert.
Raises:
ValueError: If *after_level* is not found in the parent's
effective map, if *new_level* already exists, or if
either argument is empty/whitespace.
"""
if not new_level or not new_level.strip():
raise ValueError("new_level must be a non-empty string")
if not after_level or not after_level.strip():
raise ValueError("after_level must be a non-empty string")
after_stripped = after_level.strip()
known = set(self._parent.effective_levels().keys())
known.update(new_name for _, new_name in self._insertions)
if after_stripped not in known:
raise ValueError(
f"Level '{after_stripped}' not found in parent map "
f"'{self._parent.domain}'"
)
stripped = new_level.strip()
if stripped in known:
raise ValueError(
f"Level '{stripped}' already exists in map '{self._parent.domain}'"
)
self._insertions.append((after_stripped, stripped))
_log.debug(
"detail_level_map_builder.insert_after",
after_level=after_stripped,
new_level=stripped,
domain=self._domain,
)
def build(self) -> DetailLevelMap:
"""Build the child DetailLevelMap with insertions applied.
Returns:
A new DetailLevelMap whose levels include all parent levels
plus inserted levels, with integer depths reassigned
consecutively.
Raises:
ValueError: If a duplicate level name would be introduced
(caught by ``build_effective_map``).
"""
effective = build_effective_map(self._parent, self._insertions)
# build_effective_map already validates no duplicate names
levels = {name: depth for name, depth in effective}
max_depth = max(levels.values()) if levels else 0
result = DetailLevelMap(
domain=self._domain,
parent=self._parent,
levels=levels,
max_depth=max_depth,
)
_log.debug(
"detail_level_map_builder.built",
domain=self._domain,
num_levels=len(levels),
max_depth=max_depth,
)
return result
def build_effective_map(
parent: DetailLevelMap,
insertions: list[tuple[str, str]] | tuple[tuple[str, str], ...],
) -> tuple[tuple[str, int], ...]:
"""Build the effective (name, depth) pairs for a child map.
Takes a parent map and a sequence of (after_level, new_level)
insertions, applies them in order, and returns the fully
reassigned depth mappings as consecutive integers starting from 0.
Args:
parent: Parent DetailLevelMap to extend.
insertions: Sequence of (after_level, new_level) pairs.
Returns:
Tuple of (name, depth) pairs ordered by depth.
Raises:
ValueError: If an after_level is not found in the current map,
or if a duplicate level name would be introduced.
"""
# Start with parent's effective levels sorted by depth
parent_levels = parent.effective_levels()
sorted_levels = sorted(parent_levels.items(), key=lambda x: x[1])
# Build ordered list of level names with O(1) membership set
ordered_names: list[str] = [name for name, _depth in sorted_levels]
known: set[str] = set(ordered_names)
# Apply insertions in order
for after_level, new_level in insertions:
if after_level not in known:
raise ValueError(
f"Level '{after_level}' not found in effective map "
f"during insertion of '{new_level}'"
)
if new_level in known:
raise ValueError(f"Duplicate level name '{new_level}' during insertion")
idx = ordered_names.index(after_level)
ordered_names.insert(idx + 1, new_level)
known.add(new_level)
# Reassign consecutive integers
return tuple((name, i) for i, name in enumerate(ordered_names))
# ---------------------------------------------------------------------------
# Layer 2 maps: uko-oo:, uko-func:, uko-proc:
# ---------------------------------------------------------------------------
def _build_oo_map() -> DetailLevelMap:
"""Build the uko-oo: DetailLevelMap.
Inserts CLASS_HIERARCHY after MEMBER_LISTING (depth 3) and
VISIBILITY_ANNOTATED after SIGNATURES_WITH_DOCS (depth 7).
Based on specification.md ~lines 24987-25004.
"""
builder = DetailLevelMapBuilder(CODE_DETAIL_LEVEL_MAP, "uko-oo:")
builder.insert_after("MEMBER_LISTING", "CLASS_HIERARCHY")
# Note: issue #575 describes this as "SIGNATURES_WITH_DOCS at depth 5"
# but the spec (lines 24987-25004) defines VISIBILITY_ANNOTATED as
# the OO-specific insertion. SIGNATURES_WITH_DOCS already exists in
# the parent uko-code: map.
builder.insert_after("SIGNATURES_WITH_DOCS", "VISIBILITY_ANNOTATED")
return builder.build()
def _build_passthrough_map(domain: str) -> DetailLevelMap:
"""Build a passthrough child DetailLevelMap that inherits all parent levels.
Used for paradigm maps (e.g., ``uko-func:``, ``uko-proc:``) that inherit
all named levels from ``uko-code:`` without additional insertions. Levels
are resolved via ``effective_levels()`` through the parent chain.
Args:
domain: UKO namespace for the child map (e.g., ``"uko-func:"``).
Returns:
A new ``DetailLevelMap`` with empty own levels and
``max_depth`` inherited from the code-domain constant.
"""
return DetailLevelMap(
domain=domain,
parent=CODE_DETAIL_LEVEL_MAP,
levels={},
max_depth=_CODE_MAX_DEPTH,
)
#: Layer 2 OO map. Read-only after module initialisation.
OO_DETAIL_LEVEL_MAP: DetailLevelMap = _build_oo_map()
#: Layer 2 functional map. Read-only after module initialisation.
FUNC_DETAIL_LEVEL_MAP: DetailLevelMap = _build_passthrough_map("uko-func:")
#: Layer 2 procedural map. Read-only after module initialisation.
PROC_DETAIL_LEVEL_MAP: DetailLevelMap = _build_passthrough_map("uko-proc:")
+486
View File
@@ -0,0 +1,486 @@
"""UKO Layer 2 paradigm vocabulary definitions.
Defines the OWL class and property vocabularies for the three Layer 2
paradigm specializations: Object-Oriented (``uko-oo:``), Functional
(``uko-func:``), and Procedural (``uko-proc:``).
Each vocabulary declares:
- Classes with their ``rdfs:subClassOf`` parent URIs
- Object properties with domain/range/subPropertyOf
- Namespace prefix and IRI
Based on ``docs/specification.md`` ~lines 42384-42468.
"""
from __future__ import annotations
import functools
import structlog
from pydantic import BaseModel, ConfigDict, Field, field_validator
__all__: list[str] = [
"UKO_ATOM",
"UKO_BOUNDARY",
"UKO_CODE_CALLABLE",
"UKO_CODE_TYPE_DEFINITION",
"UKO_CONTAINER",
"UKO_DEPENDS_ON",
"UKO_FUNC_IRI",
"UKO_FUNC_PREFIX",
"UKO_OO_IRI",
"UKO_OO_PREFIX",
"UKO_PROC_IRI",
"UKO_PROC_PREFIX",
"ParadigmVocabulary",
"VocabularyClass",
"VocabularyProperty",
"get_func_vocabulary",
"get_oo_vocabulary",
"get_proc_vocabulary",
]
_log = structlog.get_logger(__name__)
# ---------------------------------------------------------------------------
# Namespace constants (spec line 41900-41902)
# ---------------------------------------------------------------------------
UKO_OO_PREFIX = "uko-oo:"
UKO_OO_IRI = "https://cleveragents.ai/ontology/uko/oo#"
UKO_FUNC_PREFIX = "uko-func:"
UKO_FUNC_IRI = "https://cleveragents.ai/ontology/uko/func#"
UKO_PROC_PREFIX = "uko-proc:"
UKO_PROC_IRI = "https://cleveragents.ai/ontology/uko/proc#"
# Layer 1 URIs that Layer 2 depends on (will be wired when Layer 1 lands)
UKO_CODE_TYPE_DEFINITION = "https://cleveragents.ai/ontology/uko/code#TypeDefinition"
UKO_CODE_CALLABLE = "https://cleveragents.ai/ontology/uko/code#Callable"
# Layer 0 URIs
UKO_CONTAINER = "https://cleveragents.ai/ontology/uko#Container"
UKO_ATOM = "https://cleveragents.ai/ontology/uko#Atom"
UKO_BOUNDARY = "https://cleveragents.ai/ontology/uko#Boundary"
UKO_DEPENDS_ON = "https://cleveragents.ai/ontology/uko#dependsOn"
# ---------------------------------------------------------------------------
# Shared URI validation helper
# ---------------------------------------------------------------------------
def _validate_http_uri(uri: str, *, field_name: str = "URI") -> str:
"""Validate that *uri* uses an HTTP or HTTPS scheme.
This is a **scheme-only** check. These URIs are used as ontology
identifiers and are never fetched at runtime, so full URL parsing
and SSRF mitigation are not required here.
Args:
uri: The URI string to validate.
field_name: Human-readable field name for error messages.
Returns:
The validated URI string (unchanged).
Raises:
ValueError: If *uri* does not start with ``http://`` or ``https://``.
"""
if not uri.startswith(("http://", "https://")):
raise ValueError(f"{field_name} must use http(s) scheme, got: {uri!r}")
return uri
# ---------------------------------------------------------------------------
# Vocabulary models
# ---------------------------------------------------------------------------
class VocabularyClass(BaseModel, frozen=True):
"""An OWL class definition within a paradigm vocabulary.
Represents a single ``owl:Class`` with its label, comment, and
``rdfs:subClassOf`` parent URIs.
Args:
uri: Full IRI of this class.
label: Human-readable label (``rdfs:label``).
comment: Description (``rdfs:comment``).
parent_uris: Tuple of ``rdfs:subClassOf`` parent IRIs.
"""
uri: str = Field(
...,
min_length=1,
description="Full IRI of this OWL class",
)
label: str = Field(
...,
min_length=1,
description="rdfs:label for this class",
)
comment: str = Field(
default="",
description="rdfs:comment describing this class",
)
parent_uris: tuple[str, ...] = Field(
default=(),
description="rdfs:subClassOf parent IRIs",
)
@field_validator("uri")
@classmethod
def _validate_uri(cls: type[VocabularyClass], v: str) -> str:
"""Validate URI uses an HTTP(S) scheme."""
return _validate_http_uri(v, field_name="class URI")
@field_validator("parent_uris")
@classmethod
def _validate_parent_uris(
cls: type[VocabularyClass],
v: tuple[str, ...],
) -> tuple[str, ...]:
"""Validate all parent URIs use an HTTP(S) scheme."""
for uri in v:
_validate_http_uri(uri, field_name="parent_uris entry")
return v
model_config = ConfigDict(str_strip_whitespace=True)
class VocabularyProperty(BaseModel, frozen=True):
"""An OWL property definition within a paradigm vocabulary.
Represents an ``owl:ObjectProperty`` with domain, range, and
optional ``rdfs:subPropertyOf``.
Args:
uri: Full IRI of this property.
label: Human-readable label.
domain_uri: The ``rdfs:domain`` class IRI.
range_uri: The ``rdfs:range`` class IRI.
sub_property_of: Optional ``rdfs:subPropertyOf`` parent IRI.
"""
uri: str = Field(
...,
min_length=1,
description="Full IRI of this OWL property",
)
label: str = Field(
...,
min_length=1,
description="rdfs:label for this property",
)
domain_uri: str = Field(
...,
min_length=1,
description="rdfs:domain class IRI",
)
range_uri: str = Field(
...,
min_length=1,
description="rdfs:range class IRI",
)
sub_property_of: str | None = Field(
default=None,
description="rdfs:subPropertyOf parent IRI, or None if not a sub-property",
)
@field_validator("uri")
@classmethod
def _validate_uri(cls: type[VocabularyProperty], v: str) -> str:
"""Validate URI uses an HTTP(S) scheme."""
return _validate_http_uri(v, field_name="property URI")
@field_validator("domain_uri")
@classmethod
def _validate_domain_uri(cls: type[VocabularyProperty], v: str) -> str:
"""Validate domain URI uses an HTTP(S) scheme."""
return _validate_http_uri(v, field_name="domain_uri")
@field_validator("range_uri")
@classmethod
def _validate_range_uri(cls: type[VocabularyProperty], v: str) -> str:
"""Validate range URI uses an HTTP(S) scheme."""
return _validate_http_uri(v, field_name="range_uri")
@field_validator("sub_property_of")
@classmethod
def _validate_sub_property_of(
cls: type[VocabularyProperty],
v: str | None,
) -> str | None:
"""Validate sub_property_of URI uses an HTTP(S) scheme if set."""
if v is not None:
_validate_http_uri(v, field_name="sub_property_of")
return v
model_config = ConfigDict(str_strip_whitespace=True)
class ParadigmVocabulary(BaseModel, frozen=True):
"""A complete Layer 2 paradigm vocabulary definition.
Groups all OWL classes and properties for a single paradigm
(e.g., Object-Oriented, Functional, Procedural) with its
namespace prefix and IRI.
Args:
prefix: Short prefix (e.g., ``"uko-oo:"``).
iri: Full namespace IRI.
layer: Ontology layer number (always 2 for paradigm vocabs).
parent_domain: Parent Layer 1 domain prefix.
classes: Tuple of OWL class definitions.
properties: Tuple of OWL property definitions.
"""
prefix: str = Field(
...,
min_length=1,
description="Short namespace prefix (e.g., 'uko-oo:')",
)
iri: str = Field(
...,
min_length=1,
description="Full namespace IRI",
)
layer: int = Field(
...,
ge=0,
le=7,
description="Ontology layer number (e.g., 2 for paradigm vocabs)",
)
parent_domain: str = Field(
...,
min_length=1,
description="Parent domain prefix (e.g., 'uko-code:' for Layer 2)",
)
classes: tuple[VocabularyClass, ...] = Field(
default=(),
description="OWL class definitions",
)
properties: tuple[VocabularyProperty, ...] = Field(
default=(),
description="OWL property definitions",
)
@field_validator("prefix")
@classmethod
def _validate_prefix(cls: type[ParadigmVocabulary], v: str) -> str:
"""Validate prefix is non-empty and ends with a colon."""
stripped = v.strip()
if not stripped:
raise ValueError("prefix must be a non-empty string")
if not stripped.endswith(":"):
raise ValueError(f"prefix must end with ':', got: {stripped!r}")
return stripped
@field_validator("iri")
@classmethod
def _validate_iri(cls: type[ParadigmVocabulary], v: str) -> str:
"""Validate IRI uses http or https scheme."""
return _validate_http_uri(v, field_name="vocabulary IRI")
def get_class(self, label: str) -> VocabularyClass | None:
"""Look up a class by its label.
Args:
label: The ``rdfs:label`` of the class to find.
Returns:
The matching VocabularyClass, or None if not found.
"""
for cls in self.classes:
if cls.label == label:
return cls
_log.debug("vocabulary.class_not_found", label=label, prefix=self.prefix)
return None
def get_property(self, label: str) -> VocabularyProperty | None:
"""Look up a property by its label.
Args:
label: The ``rdfs:label`` of the property to find.
Returns:
The matching VocabularyProperty, or None if not found.
"""
for prop in self.properties:
if prop.label == label:
return prop
_log.debug("vocabulary.property_not_found", label=label, prefix=self.prefix)
return None
def class_uris(self) -> tuple[str, ...]:
"""Return all class URIs in this vocabulary."""
return tuple(cls.uri for cls in self.classes)
def property_uris(self) -> tuple[str, ...]:
"""Return all property URIs in this vocabulary."""
return tuple(prop.uri for prop in self.properties)
model_config = ConfigDict(str_strip_whitespace=True)
# ---------------------------------------------------------------------------
# uko-oo: vocabulary (spec lines 42384-42411)
# ---------------------------------------------------------------------------
@functools.lru_cache(maxsize=1)
def get_oo_vocabulary() -> ParadigmVocabulary:
"""Return the Object-Oriented paradigm vocabulary.
Classes: Class, Interface, Method, Attribute.
Properties: inheritsFrom, implements.
Based on specification.md ~lines 42384-42411.
"""
return ParadigmVocabulary(
prefix=UKO_OO_PREFIX,
iri=UKO_OO_IRI,
layer=2,
parent_domain="uko-code:",
classes=(
VocabularyClass(
uri=f"{UKO_OO_IRI}Class",
label="Class",
comment="An OO class that contains methods and attributes.",
parent_uris=(UKO_CODE_TYPE_DEFINITION, UKO_CONTAINER),
),
VocabularyClass(
uri=f"{UKO_OO_IRI}Interface",
label="Interface",
comment="An interface or abstract base class.",
parent_uris=(UKO_CODE_TYPE_DEFINITION, UKO_BOUNDARY),
),
VocabularyClass(
uri=f"{UKO_OO_IRI}Method",
label="Method",
comment="A method defined within a class.",
parent_uris=(UKO_CODE_CALLABLE,),
),
VocabularyClass(
uri=f"{UKO_OO_IRI}Attribute",
label="Attribute",
comment="A class or instance attribute.",
parent_uris=(UKO_ATOM,),
),
),
properties=(
VocabularyProperty(
uri=f"{UKO_OO_IRI}inheritsFrom",
label="inheritsFrom",
domain_uri=f"{UKO_OO_IRI}Class",
range_uri=f"{UKO_OO_IRI}Class",
sub_property_of=UKO_DEPENDS_ON,
),
VocabularyProperty(
uri=f"{UKO_OO_IRI}implements",
label="implements",
domain_uri=f"{UKO_OO_IRI}Class",
range_uri=f"{UKO_OO_IRI}Interface",
sub_property_of=UKO_DEPENDS_ON,
),
),
)
# ---------------------------------------------------------------------------
# uko-func: vocabulary (spec lines 42413-42425)
# ---------------------------------------------------------------------------
@functools.lru_cache(maxsize=1)
def get_func_vocabulary() -> ParadigmVocabulary:
"""Return the Functional paradigm vocabulary.
Classes: PureFunction, TypeClass, Monad.
Based on specification.md ~lines 42413-42425.
"""
return ParadigmVocabulary(
prefix=UKO_FUNC_PREFIX,
iri=UKO_FUNC_IRI,
layer=2,
parent_domain="uko-code:",
classes=(
VocabularyClass(
uri=f"{UKO_FUNC_IRI}PureFunction",
label="PureFunction",
comment="A pure function with no side effects.",
parent_uris=(UKO_CODE_CALLABLE,),
),
VocabularyClass(
uri=f"{UKO_FUNC_IRI}TypeClass",
label="TypeClass",
comment="A type class defining a set of operations for types.",
parent_uris=(UKO_CODE_TYPE_DEFINITION, UKO_BOUNDARY),
),
VocabularyClass(
uri=f"{UKO_FUNC_IRI}Monad",
label="Monad",
comment="A monadic type encapsulating computation context.",
parent_uris=(UKO_CODE_TYPE_DEFINITION,),
),
),
properties=(),
)
# ---------------------------------------------------------------------------
# uko-proc: vocabulary (spec lines 42460-42468)
# ---------------------------------------------------------------------------
@functools.lru_cache(maxsize=1)
def get_proc_vocabulary() -> ParadigmVocabulary:
"""Return the Procedural paradigm vocabulary.
Classes: ProceduralFunction, GlobalVariable, HeaderFile,
StructDefinition, Macro.
Based on specification.md ~lines 42460-42468.
"""
return ParadigmVocabulary(
prefix=UKO_PROC_PREFIX,
iri=UKO_PROC_IRI,
layer=2,
parent_domain="uko-code:",
classes=(
VocabularyClass(
uri=f"{UKO_PROC_IRI}ProceduralFunction",
label="ProceduralFunction",
comment="A procedural function declaration.",
parent_uris=(UKO_CODE_CALLABLE,),
),
VocabularyClass(
uri=f"{UKO_PROC_IRI}GlobalVariable",
label="GlobalVariable",
comment="A global variable declaration.",
parent_uris=(UKO_ATOM,),
),
VocabularyClass(
uri=f"{UKO_PROC_IRI}HeaderFile",
label="HeaderFile",
comment="A header file defining exported declarations.",
parent_uris=(UKO_CONTAINER,),
),
VocabularyClass(
uri=f"{UKO_PROC_IRI}StructDefinition",
label="StructDefinition",
comment="A C-style struct type definition.",
parent_uris=(UKO_CODE_TYPE_DEFINITION,),
),
VocabularyClass(
uri=f"{UKO_PROC_IRI}Macro",
label="Macro",
comment="A preprocessor macro definition.",
parent_uris=(UKO_ATOM,),
),
),
properties=(),
)
@@ -0,0 +1,145 @@
"""Vocabulary registry for UKO Layer 2 paradigm vocabularies.
Provides a ``VocabularyRegistry`` that stores and retrieves
``ParadigmVocabulary`` instances by prefix or IRI.
Thread Safety
-------------
``VocabularyRegistry`` is **not** thread-safe. External
synchronisation is required if instances are shared across threads
after initial population. Typical usage creates and populates a
registry once during module initialisation under the GIL.
"""
from __future__ import annotations
from bisect import insort
import structlog
from cleveragents.acms.uko.vocabularies import ParadigmVocabulary
__all__: list[str] = ["DuplicateVocabularyError", "VocabularyRegistry"]
_log = structlog.get_logger(__name__)
class DuplicateVocabularyError(ValueError):
"""Raised when registering a vocabulary with a duplicate prefix or IRI."""
class VocabularyRegistry:
"""Registry of Layer 2 paradigm vocabularies.
Provides lookup by prefix or IRI and lists all registered
vocabularies. An internal sorted prefix list is maintained so
that ``list_prefixes`` and ``list_all`` avoid re-sorting on
every call.
Mutation Safety
~~~~~~~~~~~~~~~
``register()`` and ``unregister()`` mutate internal state. The
registry does **not** make defensive copies of vocabularies on
retrieval; callers receive the original frozen
``ParadigmVocabulary`` instances.
Args:
vocabularies: Initial vocabularies to register.
"""
def __init__(
self,
vocabularies: tuple[ParadigmVocabulary, ...] = (),
) -> None:
self._by_prefix: dict[str, ParadigmVocabulary] = {}
self._by_iri: dict[str, ParadigmVocabulary] = {}
self._sorted_prefixes: list[str] = []
for vocab in vocabularies:
self.register(vocab)
def register(self, vocabulary: ParadigmVocabulary) -> None:
"""Register a paradigm vocabulary.
Args:
vocabulary: The vocabulary to register.
Raises:
DuplicateVocabularyError: If a vocabulary with the same
prefix or IRI is already registered.
"""
if vocabulary.prefix in self._by_prefix:
raise DuplicateVocabularyError(
f"Vocabulary with prefix '{vocabulary.prefix}' already registered"
)
if vocabulary.iri in self._by_iri:
raise DuplicateVocabularyError(
f"Vocabulary with IRI '{vocabulary.iri}' already registered"
)
self._by_prefix[vocabulary.prefix] = vocabulary
self._by_iri[vocabulary.iri] = vocabulary
insort(self._sorted_prefixes, vocabulary.prefix)
_log.debug(
"vocabulary_registry.registered",
prefix=vocabulary.prefix,
iri=vocabulary.iri,
num_classes=len(vocabulary.classes),
num_properties=len(vocabulary.properties),
)
def unregister(self, prefix: str) -> bool:
"""Remove a vocabulary by prefix.
Args:
prefix: The prefix of the vocabulary to remove.
Returns:
``True`` if the vocabulary was removed, ``False`` if the
prefix was not registered.
"""
vocab = self._by_prefix.pop(prefix, None)
if vocab is None:
return False
self._by_iri.pop(vocab.iri, None)
self._sorted_prefixes.remove(prefix)
_log.debug("vocabulary_registry.unregistered", prefix=prefix)
return True
def get_by_prefix(self, prefix: str) -> ParadigmVocabulary | None:
"""Look up a vocabulary by its short prefix.
Args:
prefix: Short prefix (e.g., ``"uko-oo:"``).
Returns:
The matching vocabulary, or None if not found.
"""
return self._by_prefix.get(prefix)
def get_by_iri(self, iri: str) -> ParadigmVocabulary | None:
"""Look up a vocabulary by its full IRI.
Args:
iri: Full namespace IRI.
Returns:
The matching vocabulary, or None if not found.
"""
return self._by_iri.get(iri)
def list_prefixes(self) -> tuple[str, ...]:
"""Return all registered prefixes in sorted order."""
return tuple(self._sorted_prefixes)
def list_all(self) -> tuple[ParadigmVocabulary, ...]:
"""Return all registered vocabularies sorted by prefix."""
return tuple(self._by_prefix[p] for p in self._sorted_prefixes)
def __len__(self) -> int:
"""Return the number of registered vocabularies."""
return len(self._by_prefix)
def __contains__(self, item: object) -> bool:
"""Check if a prefix or IRI is registered."""
if isinstance(item, str):
return item in self._by_prefix or item in self._by_iri
return False
@@ -5,6 +5,9 @@ Abstraction Layer (BAL) protocols used by actors, skills, strategies,
and the Context Assembly Pipeline:
CRP types (from :mod:`~cleveragents.domain.models.acms.crp`):
- ``DetailDepth`` -- Type alias for ``int | str`` detail depth values
- ``DetailLevelError`` -- Raised when a detail level name cannot be resolved
- ``DetailLevelCycleError`` -- Raised when a circular parent chain is detected
- ``DetailLevelMap`` -- Named-level-to-integer resolution with inheritance
- ``ContextRequest`` -- Structured request for context via the CRP
- ``ContextFragment`` -- Atomic unit of retrieved context
@@ -120,6 +123,9 @@ from cleveragents.domain.models.acms.crp import (
ContextBudget,
ContextFragment,
ContextRequest,
DetailDepth,
DetailLevelCycleError,
DetailLevelError,
DetailLevelMap,
FragmentProvenance,
)
@@ -226,6 +232,9 @@ __all__: list[str] = [
"ContextStrategy",
"ContextStrategyResult",
"ContextTier",
"DetailDepth",
"DetailLevelCycleError",
"DetailLevelError",
"DetailLevelMap",
"DockerComposeAnalyzer",
"DomainDescriptor",
+50 -126
View File
@@ -16,141 +16,45 @@ Context Assembly Pipeline and produced by context strategies.
| ``ContextBudget`` | Token budget management with reservation |
| ``AssembledContext`` | Fused, budget-respecting context payload |
## DetailDepth
At the universal level (UKO Layer 0), detail depth is a **non-negative
integer** -- 0 is the most minimal representation and each increment
reveals more. There is no fixed upper bound; the maximum meaningful
depth depends on the domain and the complexity of the information unit.
Each UKO domain extension registers a ``DetailLevelMap`` -- a table of
named labels mapped to specific integer depths. Maps are inherited:
a child map includes all entries from its parent map, and may insert
additional levels at any integer position.
``DetailLevelMap``, ``DetailDepth``, ``DetailLevelError``, and
``DetailLevelCycleError`` are defined in the ``detail_level`` sibling
module and re-exported here for backward compatibility.
Based on ``docs/specification.md`` ACMS / CRP sections and ADR-014.
"""
from __future__ import annotations
from typing import Any
from typing import Annotated, Any
from pydantic import (
BaseModel,
ConfigDict,
Field,
StringConstraints,
field_validator,
model_validator,
)
from cleveragents.domain.models.acms.detail_level import ( # re-exported
DetailDepth,
DetailLevelCycleError,
DetailLevelError,
DetailLevelMap,
)
from cleveragents.domain.models.core.project import TemporalScope
# ---------------------------------------------------------------------------
# DetailLevelMap
# ---------------------------------------------------------------------------
class DetailLevelMap(BaseModel):
"""Maps named detail levels to integer depths for a UKO domain.
Inherited: a child map includes all entries from its parent map,
and may insert additional levels at any integer position.
Example::
code_map = DetailLevelMap(
domain="uko-code:",
parent=None,
levels={"MODULE_LISTING": 0, "FULL_SOURCE": 9},
max_depth=9,
)
assert code_map.resolve(4) == 4
assert code_map.resolve("MODULE_LISTING") == 0
"""
domain: str = Field(
...,
min_length=1,
description="UKO namespace (e.g., 'uko-code:', 'uko-py:')",
)
parent: DetailLevelMap | None = Field(
default=None,
description="Parent map to inherit from",
)
levels: dict[str, int] = Field(
default_factory=dict,
description="Named level -> integer depth",
)
max_depth: int = Field(
...,
ge=0,
description="Maximum meaningful depth for this domain",
)
@field_validator("levels")
@classmethod
def validate_levels(
cls: type[DetailLevelMap],
v: dict[str, int],
) -> dict[str, int]:
"""Ensure all level values are non-negative integers."""
for name, depth in v.items():
if depth < 0:
raise ValueError(f"Detail level '{name}' has negative depth {depth}")
return v
def resolve(self, depth: int | str) -> int:
"""Resolve a named level or integer to an integer depth.
Args:
depth: Either a raw integer (clamped to ``max_depth``) or
a named level string looked up in this map, then in
parent maps.
Returns:
Resolved integer depth.
Raises:
ValueError: If *depth* is a string not found in this map
or any ancestor.
"""
if isinstance(depth, int):
return min(depth, self.max_depth)
# Look up named level in this map, then parent maps
if depth in self.levels:
return self.levels[depth]
if self.parent is not None:
return self.parent.resolve(depth)
raise ValueError(f"Unknown detail level: {depth}")
def register(self, name: str, value: int) -> None:
"""Register a custom detail level.
Args:
name: Named level label.
value: Integer depth to map to.
Raises:
ValueError: If *value* is negative.
"""
if value < 0:
raise ValueError(f"Detail level '{name}' has negative depth {value}")
self.levels[name] = value
def effective_levels(self) -> dict[str, int]:
"""Return the full merged level map including inherited entries.
Parent entries are included first; child entries override any
collisions.
"""
merged = self.parent.effective_levels() if self.parent is not None else {}
merged.update(self.levels)
return merged
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
)
__all__: list[str] = [
"AssembledContext",
"ContextBudget",
"ContextFragment",
"ContextRequest",
"DetailDepth",
"DetailLevelCycleError",
"DetailLevelError",
"DetailLevelMap",
"FragmentProvenance",
]
# ---------------------------------------------------------------------------
@@ -286,6 +190,19 @@ class ContextFragment(BaseModel, frozen=True):
# ContextRequest
# ---------------------------------------------------------------------------
#: Upper bound for list fields on ``ContextRequest`` to prevent
#: memory exhaustion from excessively large payloads.
_MAX_LIST_ITEMS: int = 500
#: Upper bound for the natural-language ``query`` string.
_MAX_QUERY_LENGTH: int = 10_000
#: Upper bound for individual string items inside list fields.
_MAX_ITEM_LENGTH: int = 2_000
#: Constrained string type for list items in ``ContextRequest``.
_BoundedStr = Annotated[str, StringConstraints(max_length=_MAX_ITEM_LENGTH)]
class ContextRequest(BaseModel):
"""A structured request for context, issued by an actor or skill.
@@ -301,20 +218,24 @@ class ContextRequest(BaseModel):
# === What to find ===
query: str | None = Field(
default=None,
max_length=_MAX_QUERY_LENGTH,
description="Natural language query",
)
entities: list[str] = Field(
entities: list[_BoundedStr] = Field(
default_factory=list,
max_length=_MAX_LIST_ITEMS,
description="Named entities to focus on",
)
uko_types: list[str] = Field(
uko_types: list[_BoundedStr] = Field(
default_factory=list,
max_length=_MAX_LIST_ITEMS,
description="UKO types to filter",
)
# === Scope control ===
focus: list[str] = Field(
focus: list[_BoundedStr] = Field(
default_factory=list,
max_length=_MAX_LIST_ITEMS,
description="URIs or identifiers of specific items to focus on",
)
breadth: int = Field(
@@ -322,7 +243,7 @@ class ContextRequest(BaseModel):
ge=0,
description="How many hops outward in the dependency/reference graph",
)
depth: int | str = Field(
depth: DetailDepth = Field(
default=3,
description=(
"How much detail to include for each item found. "
@@ -351,12 +272,14 @@ class ContextRequest(BaseModel):
)
# === Strategy hints ===
preferred_strategies: list[str] = Field(
preferred_strategies: list[_BoundedStr] = Field(
default_factory=list,
max_length=_MAX_LIST_ITEMS,
description="Preferred context strategies to use",
)
required_backends: list[str] = Field(
required_backends: list[_BoundedStr] = Field(
default_factory=list,
max_length=_MAX_LIST_ITEMS,
description="Required data backends (text, vector, graph)",
)
@@ -369,6 +292,7 @@ class ContextRequest(BaseModel):
)
purpose: str = Field(
default="",
max_length=_MAX_ITEM_LENGTH,
description="Why is this context needed?",
)
@@ -376,8 +300,8 @@ class ContextRequest(BaseModel):
@classmethod
def validate_depth(
cls: type[ContextRequest],
v: int | str,
) -> int | str:
v: DetailDepth,
) -> DetailDepth:
"""Ensure integer depths are non-negative."""
if isinstance(v, int) and v < 0:
raise ValueError("depth must be non-negative when specified as integer")
@@ -0,0 +1,293 @@
"""Detail-level map and resolution for UKO domains.
``DetailLevelMap`` maps named detail levels to integer depths, supports
parent-chain inheritance, cycle detection, and immutable level storage
via ``MappingProxyType``.
## DetailDepth
At the universal level (UKO Layer 0), detail depth is a **non-negative
integer** -- 0 is the most minimal representation and each increment
reveals more. Each UKO domain extension registers a ``DetailLevelMap``
that maps named labels to specific integer depths. Maps are inherited:
a child map includes all entries from its parent map.
Based on ``docs/specification.md`` ACMS / CRP sections and ADR-014.
"""
from __future__ import annotations
import copy
import threading
from collections.abc import Mapping
from types import MappingProxyType
from typing import Any
from pydantic import (
BaseModel,
ConfigDict,
Field,
PrivateAttr,
field_serializer,
field_validator,
model_validator,
)
# ---------------------------------------------------------------------------
# Public type alias
# ---------------------------------------------------------------------------
#: Detail depth can be a raw non-negative integer **or** a named level
#: string (e.g., ``"SIGNATURES"``) resolved via the active
#: ``DetailLevelMap``.
DetailDepth = int | str
# ---------------------------------------------------------------------------
# Domain-specific exceptions
# ---------------------------------------------------------------------------
class DetailLevelError(ValueError):
"""Raised when a detail level name cannot be resolved."""
class DetailLevelCycleError(DetailLevelError):
"""Raised when a circular parent chain is detected."""
# ---------------------------------------------------------------------------
# DetailLevelMap
# ---------------------------------------------------------------------------
class DetailLevelMap(BaseModel):
"""Maps named detail levels to integer depths for a UKO domain.
Inherited: a child map includes all entries from its parent map,
and may insert additional levels at any integer position.
The ``levels`` field is stored as a ``MappingProxyType`` to prevent
accidental mutation of module-level constant maps. Use
``register()`` to add levels after construction (creates a new
immutable snapshot each time).
Example::
code_map = DetailLevelMap(
domain="uko-code:",
parent=None,
levels={"MODULE_LISTING": 0, "FULL_SOURCE": 9},
max_depth=9,
)
assert code_map.resolve(4) == 4
assert code_map.resolve("MODULE_LISTING") == 0
"""
domain: str = Field(
...,
min_length=1,
description="UKO namespace (e.g., 'uko-code:', 'uko-py:')",
)
parent: DetailLevelMap | None = Field(
default=None,
description="Parent map to inherit from",
)
levels: Mapping[str, int] = Field(
default_factory=dict,
description=(
"Named level -> integer depth. "
"Accepts a ``dict`` on construction but is stored at runtime "
"as a ``MappingProxyType`` (read-only view) by the "
"``_freeze_levels`` model validator."
),
)
max_depth: int = Field(
...,
ge=0,
description="Maximum meaningful depth for this domain",
)
@field_validator("levels", mode="before")
@classmethod
def validate_levels(
cls: type[DetailLevelMap],
v: Mapping[str, int],
) -> dict[str, int]:
"""Validate non-negative depths and normalise to a plain dict.
Accepts any ``Mapping`` (``dict``, ``MappingProxyType``, etc.)
on construction and during ``validate_assignment`` round-trips.
Always returns a plain ``dict`` for the model validator to freeze.
"""
plain = dict(v)
for name, depth in plain.items():
if depth < 0:
raise ValueError(f"Detail level '{name}' has negative depth {depth}")
return plain
@model_validator(mode="after")
def _freeze_levels(self) -> DetailLevelMap:
"""Replace the mutable levels dict with a read-only proxy.
Runs on initial construction and on every ``validate_assignment``
cycle (Pydantic 2.12+ re-runs model validators on assignment).
The ``__setattr__`` override provides an additional
defense-in-depth safety net.
"""
object.__setattr__(self, "levels", MappingProxyType(dict(self.levels)))
return self
def __setattr__(self, name: str, value: Any) -> None:
"""Re-freeze ``levels`` on assignment as a defensive measure.
Pydantic 2.12+ re-runs both field *and* model validators on
``validate_assignment``, so the ``_freeze_levels`` model
validator already handles the normal case. This override is
kept as defense-in-depth in case the Pydantic behavior changes
in a future release. It re-freezes from the validated
``self.levels`` (not the raw *value*) so any normalisation
applied by the field validator is preserved.
"""
super().__setattr__(name, value)
if (
name == "levels"
and isinstance(self.levels, (dict, Mapping))
and not isinstance(self.levels, MappingProxyType)
):
object.__setattr__(self, "levels", MappingProxyType(dict(self.levels)))
def __copy__(self) -> DetailLevelMap:
"""Support ``copy.copy()`` despite unpicklable MappingProxyType.
Constructs a new instance directly (not via ``model_copy``)
to avoid infinite recursion: ``model_copy(deep=False)``
internally calls ``__copy__``.
"""
return DetailLevelMap(
domain=self.domain,
parent=self.parent,
levels=dict(self.levels),
max_depth=self.max_depth,
)
def __deepcopy__(self, memo: dict[int, Any] | None = None) -> DetailLevelMap:
"""Support ``copy.deepcopy()`` despite unpicklable fields.
Constructs a new instance directly (not via ``model_copy``)
to avoid infinite recursion. The ``_register_lock``
(a ``PrivateAttr``) gets a fresh instance automatically.
"""
if memo is None:
memo = {}
return DetailLevelMap(
domain=self.domain,
parent=copy.deepcopy(self.parent, memo),
levels=copy.deepcopy(dict(self.levels), memo),
max_depth=self.max_depth,
)
@field_serializer("levels")
@classmethod
def _serialize_levels(
cls: type[DetailLevelMap],
v: Mapping[str, int],
) -> dict[str, int]:
"""Serialize MappingProxyType back to a plain dict for JSON."""
return dict(v)
def resolve(self, depth: DetailDepth) -> int:
"""Resolve a named level or integer to an integer depth.
Args:
depth: Either a raw integer (clamped to ``max_depth``) or
a named level string looked up in this map, then in
parent maps.
Returns:
Resolved integer depth.
Raises:
ValueError: If *depth* is a string not found in this map
or any ancestor, or if a cycle is detected in the
parent chain.
"""
if isinstance(depth, int):
return max(0, min(depth, self.max_depth))
visited: set[int] = set()
current: DetailLevelMap | None = self
while current is not None:
map_id = id(current)
if map_id in visited:
raise DetailLevelCycleError(
f"Cycle detected in DetailLevelMap parent chain "
f"while resolving '{depth}'"
)
visited.add(map_id)
if depth in current.levels:
return current.levels[depth]
current = current.parent
raise DetailLevelError(f"Unknown detail level: {depth}")
#: Per-instance lock for ``register()`` thread safety.
_register_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
def register(self, name: str, value: int) -> None:
"""Register a custom detail level.
Creates a new immutable snapshot of the levels mapping.
Thread-safe via an internal lock.
Args:
name: Named level label.
value: Integer depth to map to.
Raises:
ValueError: If *value* is negative or exceeds ``max_depth``.
"""
if value < 0:
raise ValueError(f"Detail level '{name}' has negative depth {value}")
if value > self.max_depth:
raise ValueError(
f"Detail level '{name}' depth {value} exceeds "
f"max_depth {self.max_depth}"
)
with self._register_lock:
updated = dict(self.levels)
updated[name] = value
object.__setattr__(self, "levels", MappingProxyType(updated))
def effective_levels(
self, *, _visited: set[int] | None = None
) -> MappingProxyType[str, int]:
"""Return the full merged level map including inherited entries.
Returns an immutable ``MappingProxyType`` so callers cannot
accidentally mutate inherited level data. Parent entries are
included first; child entries override any collisions.
Raises:
DetailLevelCycleError: If a circular parent chain is
detected.
"""
if _visited is None:
_visited = set()
map_id = id(self)
if map_id in _visited:
raise DetailLevelCycleError(
"Cycle detected in DetailLevelMap parent chain "
"during effective_levels()"
)
_visited.add(map_id)
merged: dict[str, int] = (
dict(self.parent.effective_levels(_visited=_visited))
if self.parent is not None
else {}
)
merged.update(self.levels)
return MappingProxyType(merged)
model_config = ConfigDict(
str_strip_whitespace=True,
validate_assignment=True,
arbitrary_types_allowed=True,
)