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