feat(acms): implement UKO Layer 1 Domain Ontologies (uko-code, uko-doc, uko-data, uko-infra)
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 / security (pull_request) Successful in 33s
CI / typecheck (pull_request) Successful in 38s
CI / unit_tests (pull_request) Successful in 2m26s
CI / docker (pull_request) Successful in 40s
CI / integration_tests (pull_request) Successful in 3m29s
CI / coverage (pull_request) Successful in 4m30s
CI / benchmark-regression (pull_request) Has been cancelled

Implement the four Layer 1 domain-specific OWL/Turtle ontology vocabularies
that specialize Layer 0 universal concepts for specific knowledge domains.

Ontology (docs/ontology/uko.ttl):
- Added uko-doc: namespace with 17 classes (Document, Part, Chapter, Section,
  Subsection, Paragraph, Sentence, CodeBlock, Citation, Figure, Table,
  Footnote, Annotation, Bookmark, CrossReference, PageBreak, SectionBreak),
  5 properties, and 4 relationships
- Added uko-data: namespace with 13 classes (Schema, Table, Column, View,
  StoredProcedure, Constraint, Index, ForeignKey, Trigger, Annotation,
  Bookmark, PartitionBoundary, ShardBoundary), 6 properties, and
  5 relationships
- Added uko-infra: namespace with 7 classes (Service, Network, Endpoint,
  ConfigKey, Volume, FirewallRule, SubnetBoundary) and 2 relationships
- All classes use rdfs:subClassOf from Layer 0 base classes

Domain registry (ontology_registry.py):
- DomainDescriptor dataclass with namespace IRI, prefix, layer, classes,
  superclass mappings, and DetailLevelMap
- Registry functions: get_domain(), list_domains(), get_layer1_domains()
- DetailLevelMap chain builder for hierarchical depth resolution
- Turtle validation with TurtleValidationError (no rdflib dependency)
- All DetailLevelMap data inlined to maintain DIP compliance (domain
  layer does not import from application layer)

DetailLevelMap presets (depth_breadth_projection.py):
- code_detail_map: 10 spec-complete levels (depths 0-9)
- docs_detail_map: 11 spec-complete levels (depths 0-10)
- database_detail_map: 12 spec-complete levels (depths 0-11)
- infra_detail_map: 9 spec-complete levels (depths 0-8)

Tests:
- 31 BDD scenarios in uko_ontology_registry.feature covering domain
  lookup, all DetailLevelMap levels, inheritance chains, Turtle
  validation, Universal View Guarantee, and negative/edge cases
- 6 Robot Framework integration tests
- Updated existing tests: depth_breadth_projection.feature (TABLE_LISTING
  depth 0->1, added SCHEMA_LISTING), uko_ontology.feature (Layer 1
  node count 8->67)

Spec reference: docs/specification.md §41830-42332

ISSUES CLOSED: #574
This commit is contained in:
2026-03-06 15:40:31 +00:00
parent 95c9c9f3a4
commit 04f24b39c0
12 changed files with 2078 additions and 35 deletions
+9
View File
@@ -2,6 +2,15 @@
## Unreleased
- Implemented UKO Layer 1 Domain Ontologies (`uko-doc:`, `uko-data:`, `uko-infra:`)
in the OWL/Turtle ontology file (`docs/ontology/uko.ttl`). Added 17 `uko-doc:` classes
(Document, Section, Paragraph, Citation, etc.), 13 `uko-data:` classes (Table, Column,
ForeignKey, View, etc.), and 7 `uko-infra:` classes (Service, Endpoint, Port, etc.) with
all spec-mandated properties and relationships. Updated all four DetailLevelMap presets
(`code_detail_map`, `docs_detail_map`, `database_detail_map`, `infra_detail_map`) to
be spec-complete with every named level. Added `OntologyRegistry` with domain lookup,
Layer 1 listing, DetailLevelMap inheritance chain building, and Turtle syntax validation.
Includes 31 Behave BDD scenarios and 6 Robot Framework integration tests. (#574)
- Added `PostgreSQLAnalyzer` and `DockerComposeAnalyzer` domain-specific analyzers
(Phase 2 of issue #588). `PostgreSQLAnalyzer` parses DDL content via regex and
extracts `uko-data:Table`, `uko-data:Column`, `uko-data:ForeignKey`, `uko-data:View`,
+372 -1
View File
@@ -1,5 +1,8 @@
@prefix uko: <https://cleveragents.ai/ontology/uko#> .
@prefix uko-code: <https://cleveragents.ai/ontology/uko/code#> .
@prefix uko-doc: <https://cleveragents.ai/ontology/uko/doc#> .
@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 rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@@ -157,7 +160,7 @@ uko-code:Import a owl:Class ;
rdfs:label "Import" ;
rdfs:comment "An import/include/require statement." .
# -- Layer 1 Properties --
# -- uko-code: Properties --
uko-code:hasReturnType a owl:DatatypeProperty ;
rdfs:domain uko-code:Callable ;
@@ -176,6 +179,374 @@ uko-code:testsCallable a owl:ObjectProperty ;
rdfs:label "testsCallable" ;
rdfs:comment "Links a test case to the callable it tests." .
# ===========================================================================
# Layer 1: Documents (uko-doc:)
# ===========================================================================
# -- Container Classes --
uko-doc:Document a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Document" ;
rdfs:comment "A complete document (article, report, manual, specification)." .
uko-doc:Part a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Part" ;
rdfs:comment "A top-level division of a document (e.g., Part I, Part II)." .
uko-doc:Chapter a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Chapter" ;
rdfs:comment "A chapter within a document or part." .
uko-doc:Section a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Section" ;
rdfs:comment "A numbered or titled section within a chapter." .
uko-doc:Subsection a owl:Class ;
rdfs:subClassOf uko-doc:Section ;
rdfs:label "Subsection" ;
rdfs:comment "A subsection nested within a section (arbitrary depth)." .
# -- Atom Classes --
uko-doc:Paragraph a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Paragraph" ;
rdfs:comment "A paragraph of prose text." .
uko-doc:CodeBlock a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "CodeBlock" ;
rdfs:comment "An inline code listing or example within a document." .
uko-doc:Figure a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Figure" ;
rdfs:comment "A figure, diagram, or image with an optional caption." .
uko-doc:Table a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Table" ;
rdfs:comment "A tabular data element within a document." .
uko-doc:BlockQuote a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "BlockQuote" ;
rdfs:comment "A quoted passage from another source." .
uko-doc:ListBlock a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "ListBlock" ;
rdfs:comment "An ordered or unordered list." .
# -- Annotation Classes --
uko-doc:Footnote a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "Footnote" ;
rdfs:comment "A footnote or endnote." .
uko-doc:Citation a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "Citation" ;
rdfs:comment "A bibliographic citation or reference." .
uko-doc:Comment a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "Comment" ;
rdfs:comment "An editorial comment or annotation (e.g., HTML comment, review note)." .
# -- Boundary Classes --
uko-doc:TableOfContents a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "TableOfContents" ;
rdfs:comment "The document's table of contents -- an interface into the document's structure." .
uko-doc:Index a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "Index" ;
rdfs:comment "A back-of-book index or keyword index." .
uko-doc:Glossary a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "Glossary" ;
rdfs:comment "A glossary of terms defined in the document." .
# -- uko-doc: Relationships --
uko-doc:discussesTopic a owl:ObjectProperty ;
rdfs:subPropertyOf uko:references ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko:InformationUnit ;
rdfs:label "discussesTopic" ;
rdfs:comment "Semantic relationship: this unit discusses a topic that is the subject of another unit." .
uko-doc:cites a owl:ObjectProperty ;
rdfs:subPropertyOf uko:references ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko-doc:Citation ;
rdfs:label "cites" ;
rdfs:comment "This unit cites a bibliographic reference." .
uko-doc:crossReferences a owl:ObjectProperty ;
rdfs:subPropertyOf uko:references ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko:InformationUnit ;
rdfs:label "crossReferences" ;
rdfs:comment "Explicit cross-reference (e.g., 'see Section 3.2')." .
uko-doc:precedes a owl:ObjectProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range uko:InformationUnit ;
rdfs:label "precedes" ;
rdfs:comment "Reading order: this unit comes before the target unit." .
# -- uko-doc: Datatype Properties --
uko-doc:headingLevel a owl:DatatypeProperty ;
rdfs:domain uko-doc:Section ;
rdfs:range xsd:integer ;
rdfs:label "headingLevel" ;
rdfs:comment "The nesting depth of this section (1 = top-level, 2 = subsection, etc.)." .
uko-doc:headingText a owl:DatatypeProperty ;
rdfs:domain uko-doc:Section ;
rdfs:range xsd:string ;
rdfs:label "headingText" ;
rdfs:comment "The title/heading text of this section." .
uko-doc:wordCount a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:integer ;
rdfs:label "wordCount" ;
rdfs:comment "Word count of the text content in this unit." .
uko-doc:language a owl:DatatypeProperty ;
rdfs:domain uko-doc:Document ;
rdfs:range xsd:string ;
rdfs:label "language" ;
rdfs:comment "Natural language of the document (e.g., 'en', 'fr')." .
uko-doc:topicKeywords a owl:DatatypeProperty ;
rdfs:domain uko:InformationUnit ;
rdfs:range xsd:string ;
rdfs:label "topicKeywords" ;
rdfs:comment "JSON-encoded list of topic keywords extracted from this unit." .
# ===========================================================================
# Layer 1: Data Schemas (uko-data:)
# ===========================================================================
# -- Container Classes --
uko-data:Database a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Database" ;
rdfs:comment "A database instance containing schemas." .
uko-data:Schema a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Schema" ;
rdfs:comment "A database schema (namespace for tables, views, etc.)." .
uko-data:Table a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Table" ;
rdfs:comment "A database table containing columns." .
uko-data:View a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "View" ;
rdfs:comment "A database view (virtual table defined by a query)." .
# -- Atom Classes --
uko-data:Column a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Column" ;
rdfs:comment "A column within a table or view." .
uko-data:StoredProcedure a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "StoredProcedure" ;
rdfs:comment "A stored procedure or function in the database." .
uko-data:Trigger a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Trigger" ;
rdfs:comment "A database trigger." .
uko-data:Migration a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "Migration" ;
rdfs:comment "A schema migration (DDL change script)." .
# -- Annotation Classes --
uko-data:Constraint a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "Constraint" ;
rdfs:comment "A column or table constraint (NOT NULL, UNIQUE, CHECK, etc.)." .
uko-data:Index a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "Index" ;
rdfs:comment "A database index on one or more columns." .
uko-data:ColumnComment a owl:Class ;
rdfs:subClassOf uko:Annotation ;
rdfs:label "ColumnComment" ;
rdfs:comment "A COMMENT ON COLUMN annotation in the database." .
# -- Boundary Classes --
uko-data:ForeignKey a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "ForeignKey" ;
rdfs:comment "A foreign key constraint -- an interface point between tables." .
uko-data:APIEndpoint a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "APIEndpoint" ;
rdfs:comment "A database-level API endpoint (e.g., PostgREST, GraphQL)." .
# -- uko-data: Relationships --
uko-data:foreignKeyTo a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-data:Column ;
rdfs:range uko-data:Column ;
rdfs:label "foreignKeyTo" ;
rdfs:comment "Foreign key relationship from this column to a column in another table." .
uko-data:viewReferences a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-data:View ;
rdfs:range uko-data:Table ;
rdfs:label "viewReferences" ;
rdfs:comment "This view's query references columns from the target table." .
uko-data:procedureAccesses a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-data:StoredProcedure ;
rdfs:range uko-data:Table ;
rdfs:label "procedureAccesses" ;
rdfs:comment "This stored procedure reads from or writes to the target table." .
uko-data:triggerFires a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-data:Trigger ;
rdfs:range uko-data:Table ;
rdfs:label "triggerFires" ;
rdfs:comment "This trigger fires on events on the target table." .
uko-data:migrationAlters a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-data:Migration ;
rdfs:range uko-data:Table ;
rdfs:label "migrationAlters" ;
rdfs:comment "This migration modifies the schema of the target table." .
# -- uko-data: Datatype Properties --
uko-data:dataType a owl:DatatypeProperty ;
rdfs:domain uko-data:Column ;
rdfs:range xsd:string ;
rdfs:label "dataType" ;
rdfs:comment "SQL data type (e.g., 'VARCHAR(255)', 'INTEGER', 'JSONB')." .
uko-data:isNullable a owl:DatatypeProperty ;
rdfs:domain uko-data:Column ;
rdfs:range xsd:boolean ;
rdfs:label "isNullable" .
uko-data:isPrimaryKey a owl:DatatypeProperty ;
rdfs:domain uko-data:Column ;
rdfs:range xsd:boolean ;
rdfs:label "isPrimaryKey" .
uko-data:rowEstimate a owl:DatatypeProperty ;
rdfs:domain uko-data:Table ;
rdfs:range xsd:long ;
rdfs:label "rowEstimate" ;
rdfs:comment "Estimated row count from database statistics." .
uko-data:viewDefinition a owl:DatatypeProperty ;
rdfs:domain uko-data:View ;
rdfs:range xsd:string ;
rdfs:label "viewDefinition" ;
rdfs:comment "The SQL query that defines this view." .
uko-data:procedureBody a owl:DatatypeProperty ;
rdfs:domain uko-data:StoredProcedure ;
rdfs:range xsd:string ;
rdfs:label "procedureBody" ;
rdfs:comment "The SQL/PL body of the stored procedure." .
# ===========================================================================
# Layer 1: Infrastructure (uko-infra:)
# ===========================================================================
# -- Container Classes --
uko-infra:Service a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "Service" ;
rdfs:comment "A deployable service or application." .
uko-infra:ConfigBlock a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "ConfigBlock" ;
rdfs:comment "A configuration block or section (e.g., a YAML map, TOML table)." .
uko-infra:DeploymentUnit a owl:Class ;
rdfs:subClassOf uko:Container ;
rdfs:label "DeploymentUnit" ;
rdfs:comment "A deployment unit (e.g., Kubernetes Deployment, Docker Compose service)." .
# -- Atom Classes --
uko-infra:ConfigKey a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "ConfigKey" ;
rdfs:comment "A configuration key-value pair." .
uko-infra:EnvironmentVariable a owl:Class ;
rdfs:subClassOf uko:Atom ;
rdfs:label "EnvironmentVariable" ;
rdfs:comment "An environment variable definition." .
# -- Boundary Classes --
uko-infra:Endpoint a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "Endpoint" ;
rdfs:comment "A network endpoint (HTTP route, gRPC service, message queue)." .
uko-infra:Port a owl:Class ;
rdfs:subClassOf uko:Boundary ;
rdfs:label "Port" ;
rdfs:comment "A network port binding." .
# -- uko-infra: Relationships --
uko-infra:connectsTo a owl:ObjectProperty ;
rdfs:subPropertyOf uko:dependsOn ;
rdfs:domain uko-infra:Service ;
rdfs:range uko-infra:Service ;
rdfs:label "connectsTo" ;
rdfs:comment "This service connects to or depends on the target service." .
uko-infra:exposes a owl:ObjectProperty ;
rdfs:domain uko-infra:Service ;
rdfs:range uko-infra:Endpoint ;
rdfs:label "exposes" ;
rdfs:comment "This service exposes the target endpoint." .
# ===========================================================================
# Layer 2: Object-Oriented Paradigm (uko-oo:)
# ===========================================================================
+2 -1
View File
@@ -161,7 +161,8 @@ Feature: Depth/Breadth Projection System and Skeleton Context Propagation
Scenario: Database detail level map has correct levels
Given the depth/breadth projection modules are available
When I create the database detail level map preset
Then the database map should resolve "TABLE_LISTING" to 0
Then the database map should resolve "SCHEMA_LISTING" to 0
And the database map should resolve "TABLE_LISTING" to 1
And the database map should resolve "FULL_CATALOG" to 11
# ---------------------------------------------------------------------------
@@ -0,0 +1,464 @@
"""Step definitions for the UKO Ontology Domain Registry feature.
Covers domain lookup, DetailLevelMap resolution (all four Layer 1
domains), inheritance chain building, Turtle syntax validation, and
the Universal View Guarantee.
Steps are prefixed with ``ontology_`` where needed to avoid collision
with existing step files.
"""
from __future__ import annotations
import re
from pathlib import Path
from behave import given, then, when
from behave.runner import Context
from cleveragents.domain.models.acms.crp import DetailLevelMap
from cleveragents.domain.models.acms.ontology_registry import (
DomainDescriptor,
build_detail_map_chain,
get_domain,
get_layer1_domains,
list_domains,
validate_turtle,
validate_turtle_file,
)
__all__: list[str] = []
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_PROJECT_ROOT = Path(__file__).resolve().parents[2]
"""Root of the project worktree (two levels up from features/steps/)."""
# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------
@given("the UKO ontology registry module is available")
def step_ontology_module_available(context: Context) -> None:
# Importing the module is the availability check.
context.ontology_error = None
context.ontology_domain = None
context.ontology_domains_list = None
context.ontology_layer1_list = None
context.ontology_chain = None
context.ontology_validation_errors = None
context.ontology_ttl_content = None
context.ontology_resolve_error = None
context.ontology_file_error = None
context.ontology_resolved_depth = None
context.ontology_custom_map = None
@given(
'a custom DetailLevelMap for domain "{domain}" '
'with parent "{parent_prefix}" and levels:'
)
def step_custom_map_with_parent(
context: Context,
domain: str,
parent_prefix: str,
) -> None:
parent_desc = get_domain(parent_prefix)
parent_map = parent_desc.detail_map
levels: dict[str, int] = {}
for row in context.table:
levels[row["level"]] = int(row["depth"])
# Determine max_depth: parent max or highest local level, whichever is bigger
local_max = max(levels.values()) if levels else 0
max_depth = max(parent_map.max_depth, local_max)
context.ontology_custom_map = DetailLevelMap(
domain=domain,
parent=parent_map,
levels=levels,
max_depth=max_depth,
)
# ---------------------------------------------------------------------------
# When steps — domain lookup
# ---------------------------------------------------------------------------
@when('I look up domain "{prefix}"')
def step_lookup_domain(context: Context, prefix: str) -> None:
context.ontology_error = None
try:
context.ontology_domain = get_domain(prefix)
except (ValueError, KeyError) as exc:
context.ontology_error = exc
@when("I look up an empty domain prefix")
def step_lookup_empty_domain(context: Context) -> None:
context.ontology_error = None
try:
context.ontology_domain = get_domain("")
except (ValueError, KeyError) as exc:
context.ontology_error = exc
@when("I list all domains")
def step_list_domains(context: Context) -> None:
context.ontology_domains_list = list_domains()
@when("I get Layer 1 domains")
def step_get_layer1(context: Context) -> None:
context.ontology_layer1_list = get_layer1_domains()
# ---------------------------------------------------------------------------
# When steps — detail map chain
# ---------------------------------------------------------------------------
@when('I build a detail map chain from "{prefix}"')
def step_build_chain_single(context: Context, prefix: str) -> None:
context.ontology_error = None
try:
context.ontology_chain = build_detail_map_chain(prefix)
except ValueError as exc:
context.ontology_error = exc
@when("I build a detail map chain with no prefixes")
def step_build_chain_empty(context: Context) -> None:
context.ontology_error = None
try:
context.ontology_chain = build_detail_map_chain()
except ValueError as exc:
context.ontology_error = exc
@when('I try to resolve level "{level}"')
def step_try_resolve_level(context: Context, level: str) -> None:
context.ontology_resolve_error = None
try:
desc: DomainDescriptor = context.ontology_domain
desc.detail_map.resolve(level)
except ValueError as exc:
context.ontology_resolve_error = exc
# Also set the generic error so negative step works
context.ontology_error = exc
@when('I resolve "{level}" on the custom map')
def step_resolve_custom_map(context: Context, level: str) -> None:
context.ontology_resolved_depth = context.ontology_custom_map.resolve(level)
# ---------------------------------------------------------------------------
# When steps — Turtle validation
# ---------------------------------------------------------------------------
@when('I validate the project Turtle file "{relpath}"')
def step_validate_project_ttl(context: Context, relpath: str) -> None:
full_path = _PROJECT_ROOT / relpath
context.ontology_validation_errors = validate_turtle_file(full_path)
@when("I validate Turtle content:")
def step_validate_turtle_content(context: Context) -> None:
context.ontology_validation_errors = validate_turtle(context.text)
@when("I validate empty Turtle content")
def step_validate_empty_turtle(context: Context) -> None:
context.ontology_validation_errors = validate_turtle("")
@when('I validate Turtle file "{path}"')
def step_validate_turtle_file_path(context: Context, path: str) -> None:
context.ontology_file_error = None
try:
context.ontology_validation_errors = validate_turtle_file(path)
except (FileNotFoundError, ValueError) as exc:
context.ontology_file_error = exc
context.ontology_error = exc
@when("I validate Turtle file with empty path")
def step_validate_turtle_empty_path(context: Context) -> None:
context.ontology_file_error = None
try:
context.ontology_validation_errors = validate_turtle_file("")
except (FileNotFoundError, ValueError) as exc:
context.ontology_file_error = exc
context.ontology_error = exc
@when('I read the project Turtle file "{relpath}"')
def step_read_project_ttl(context: Context, relpath: str) -> None:
full_path = _PROJECT_ROOT / relpath
context.ontology_ttl_content = full_path.read_text(encoding="utf-8")
# ---------------------------------------------------------------------------
# Then steps — domain descriptor assertions
# ---------------------------------------------------------------------------
@then('the domain IRI should be "{expected_iri}"')
def step_check_domain_iri(context: Context, expected_iri: str) -> None:
desc: DomainDescriptor = context.ontology_domain
assert desc.iri == expected_iri, f"Expected IRI {expected_iri!r}, got {desc.iri!r}"
@then("the domain layer should be {expected:d}")
def step_check_domain_layer(context: Context, expected: int) -> None:
desc: DomainDescriptor = context.ontology_domain
assert desc.layer == expected, f"Expected layer {expected}, got {desc.layer}"
@then('the domain classes should contain "{cls}"')
def step_check_domain_class(context: Context, cls: str) -> None:
desc: DomainDescriptor = context.ontology_domain
assert cls in desc.classes, (
f"Class {cls!r} not found in {desc.prefix} classes: {sorted(desc.classes)}"
)
@then("the domain should have {count:d} classes")
def step_check_domain_class_count(context: Context, count: int) -> None:
desc: DomainDescriptor = context.ontology_domain
assert len(desc.classes) == count, (
f"Expected {count} classes, got {len(desc.classes)}: {sorted(desc.classes)}"
)
# ---------------------------------------------------------------------------
# Then steps — error assertions
# ---------------------------------------------------------------------------
@then('a ValueError should have been raised with message "{fragment}"')
def step_check_value_error(context: Context, fragment: str) -> None:
err = context.ontology_error
assert err is not None, "Expected ValueError but none was raised"
assert isinstance(err, ValueError), f"Expected ValueError, got {type(err).__name__}"
assert fragment in str(err), (
f"Expected error message to contain {fragment!r}, got: {err}"
)
@then("a FileNotFoundError should have been raised")
def step_check_file_not_found(context: Context) -> None:
err = context.ontology_file_error or context.ontology_error
assert err is not None, "Expected FileNotFoundError but none was raised"
assert isinstance(err, FileNotFoundError), (
f"Expected FileNotFoundError, got {type(err).__name__}"
)
# ---------------------------------------------------------------------------
# Then steps — domain list assertions
# ---------------------------------------------------------------------------
@then("the domain list should have {count:d} entries")
def step_check_domain_list_count(context: Context, count: int) -> None:
lst = context.ontology_domains_list
assert len(lst) == count, f"Expected {count} domains, got {len(lst)}"
@then("the domain list should be sorted by layer then prefix")
def step_check_domain_list_sorted(context: Context) -> None:
lst = context.ontology_domains_list
keys = [(d.layer, d.prefix) for d in lst]
assert keys == sorted(keys), f"Domain list not sorted: {keys}"
@then("the Layer 1 list should have {count:d} entries")
def step_check_layer1_count(context: Context, count: int) -> None:
lst = context.ontology_layer1_list
assert len(lst) == count, f"Expected {count} Layer 1 domains, got {len(lst)}"
@then("every domain in the Layer 1 list should have layer 1")
def step_check_all_layer1(context: Context) -> None:
for d in context.ontology_layer1_list:
assert d.layer == 1, f"Domain {d.prefix} has layer {d.layer}, expected 1"
# ---------------------------------------------------------------------------
# Then steps — detail map resolution
# ---------------------------------------------------------------------------
@then('the detail map should resolve "{level}" to {expected:d}')
def step_check_detail_resolve(context: Context, level: str, expected: int) -> None:
desc: DomainDescriptor = context.ontology_domain
actual = desc.detail_map.resolve(level)
assert actual == expected, (
f"{desc.prefix} map resolved {level!r} to {actual}, expected {expected}"
)
@then("the detail map should clamp depth {raw:d} to {expected:d}")
def step_check_detail_clamp(context: Context, raw: int, expected: int) -> None:
desc: DomainDescriptor = context.ontology_domain
actual = desc.detail_map.resolve(raw)
assert actual == expected, (
f"{desc.prefix} map clamped {raw} to {actual}, expected {expected}"
)
@then("the resolved depth should be {expected:d}")
def step_check_resolved_depth(context: Context, expected: int) -> None:
actual = context.ontology_resolved_depth
assert actual == expected, f"Expected resolved depth {expected}, got {actual}"
# ---------------------------------------------------------------------------
# Then steps — chain assertions
# ---------------------------------------------------------------------------
@then('the chain should resolve "{level}" to {expected:d}')
def step_check_chain_resolve(context: Context, level: str, expected: int) -> None:
chain: DetailLevelMap = context.ontology_chain
actual = chain.resolve(level)
assert actual == expected, (
f"Chain resolved {level!r} to {actual}, expected {expected}"
)
@then("the chain max depth should be {expected:d}")
def step_check_chain_max_depth(context: Context, expected: int) -> None:
chain: DetailLevelMap = context.ontology_chain
assert chain.max_depth == expected, (
f"Chain max_depth is {chain.max_depth}, expected {expected}"
)
# ---------------------------------------------------------------------------
# Then steps — Turtle validation assertions
# ---------------------------------------------------------------------------
@then("the validation should produce {count:d} errors")
def step_check_validation_count(context: Context, count: int) -> None:
errs = context.ontology_validation_errors
assert len(errs) == count, (
f"Expected {count} validation error(s), got {len(errs)}: {errs}"
)
@then("the validation should produce at least {count:d} error")
def step_check_validation_min(context: Context, count: int) -> None:
errs = context.ontology_validation_errors
assert len(errs) >= count, (
f"Expected at least {count} error(s), got {len(errs)}: {errs}"
)
@then('the first error should contain "{fragment}"')
def step_check_first_error(context: Context, fragment: str) -> None:
errs = context.ontology_validation_errors
assert errs, "No validation errors to check"
assert fragment in errs[0], (
f"Expected first error to contain {fragment!r}, got: {errs[0]}"
)
# ---------------------------------------------------------------------------
# Then steps — Turtle content assertions
# ---------------------------------------------------------------------------
@then('the content should contain prefix "{prefix}"')
def step_check_content_prefix(context: Context, prefix: str) -> None:
content = context.ontology_ttl_content
# Match @prefix <name>: <iri> .
name = prefix.rstrip(":")
pattern = rf"@prefix\s+{re.escape(name)}\s*:"
assert re.search(pattern, content), (
f"Prefix {prefix!r} not declared in Turtle content"
)
@then('the content should contain "{text}"')
def step_check_content_text(context: Context, text: str) -> None:
content = context.ontology_ttl_content
assert text in content, f"Text {text!r} not found in Turtle content"
# ---------------------------------------------------------------------------
# Then steps — Universal View Guarantee
# ---------------------------------------------------------------------------
# Build a map of class → superclass(es) from the TTL for ancestry checks
_SUBCLASS_RE = re.compile(
r"(\S+:\S+)\s+a\s+owl:Class\s*;\s*\n\s*rdfs:subClassOf\s+([^;.]+)",
re.MULTILINE,
)
@then("every Layer 1 class should have a subClassOf chain to uko:InformationUnit")
def step_check_universal_view(context: Context) -> None:
content = context.ontology_ttl_content
# Parse subClassOf relationships
parent_map: dict[str, set[str]] = {}
for m in re.finditer(
r"(\S+:\S+)\s+a\s+owl:Class\s*;[^.]*?"
r"rdfs:subClassOf\s+([^;.]+)",
content,
re.DOTALL,
):
cls = m.group(1)
parents_str = m.group(2)
parents = {
p.strip().rstrip(";").strip() for p in parents_str.split(",") if p.strip()
}
parent_map[cls] = parents
# Layer 1 prefixes to check
layer1_prefixes = ("uko-code:", "uko-doc:", "uko-data:", "uko-infra:")
layer1_classes = [
cls for cls in parent_map if any(cls.startswith(p) for p in layer1_prefixes)
]
assert layer1_classes, "No Layer 1 classes found in TTL"
# Check each Layer 1 class can reach uko:InformationUnit
for cls in layer1_classes:
visited: set[str] = set()
queue = [cls]
found = False
while queue:
current = queue.pop()
if current == "uko:InformationUnit":
found = True
break
if current in visited:
continue
visited.add(current)
for parent in parent_map.get(current, set()):
queue.append(parent)
assert found, (
f"Class {cls} does not have a subClassOf chain to "
f"uko:InformationUnit (visited: {visited})"
)
@then("every domain detail map should resolve integer depth 0 to 0")
def step_check_all_maps_depth_zero(context: Context) -> None:
for d in context.ontology_layer1_list:
actual = d.detail_map.resolve(0)
assert actual == 0, (
f"Domain {d.prefix} resolved depth 0 to {actual}, expected 0"
)
+1 -1
View File
@@ -188,7 +188,7 @@ Feature: UKO Ontology Loader
Examples:
| layer | count |
| 0 | 18 |
| 1 | 8 |
| 1 | 67 |
| 2 | 6 |
# ---------------------------------------------------------------------------
+322
View File
@@ -0,0 +1,322 @@
@ontology @uko
Feature: UKO Ontology Domain Registry and Turtle Validation
As an ACMS developer
I need a central registry for UKO domain ontologies
So that analyzers, strategies, and the graph backend can discover
namespace IRIs, class inventories, and DetailLevelMaps for each domain
Background:
Given the UKO ontology registry module is available
# ---------------------------------------------------------------------------
# Domain Lookup
# ---------------------------------------------------------------------------
@ontology @registry
Scenario: Look up Layer 0 domain by prefix
When I look up domain "uko:"
Then the domain IRI should be "https://cleveragents.ai/ontology/uko#"
And the domain layer should be 0
And the domain classes should contain "uko:InformationUnit"
And the domain classes should contain "uko:Container"
And the domain classes should contain "uko:Atom"
And the domain classes should contain "uko:Annotation"
And the domain classes should contain "uko:Boundary"
@ontology @registry
Scenario: Look up uko-code: domain by prefix
When I look up domain "uko-code:"
Then the domain IRI should be "https://cleveragents.ai/ontology/uko/code#"
And the domain layer should be 1
And the domain classes should contain "uko-code:Module"
And the domain classes should contain "uko-code:Callable"
And the domain classes should contain "uko-code:TypeDefinition"
And the domain classes should contain "uko-code:TestCase"
And the domain classes should contain "uko-code:Import"
@ontology @registry
Scenario: Look up uko-doc: domain by prefix
When I look up domain "uko-doc:"
Then the domain IRI should be "https://cleveragents.ai/ontology/uko/doc#"
And the domain layer should be 1
And the domain should have 17 classes
And the domain classes should contain "uko-doc:Document"
And the domain classes should contain "uko-doc:Section"
And the domain classes should contain "uko-doc:Paragraph"
And the domain classes should contain "uko-doc:Citation"
And the domain classes should contain "uko-doc:TableOfContents"
@ontology @registry
Scenario: Look up uko-data: domain by prefix
When I look up domain "uko-data:"
Then the domain IRI should be "https://cleveragents.ai/ontology/uko/data#"
And the domain layer should be 1
And the domain should have 13 classes
And the domain classes should contain "uko-data:Table"
And the domain classes should contain "uko-data:Column"
And the domain classes should contain "uko-data:ForeignKey"
And the domain classes should contain "uko-data:StoredProcedure"
And the domain classes should contain "uko-data:View"
@ontology @registry
Scenario: Look up uko-infra: domain by prefix
When I look up domain "uko-infra:"
Then the domain IRI should be "https://cleveragents.ai/ontology/uko/infra#"
And the domain layer should be 1
And the domain should have 7 classes
And the domain classes should contain "uko-infra:Service"
And the domain classes should contain "uko-infra:Endpoint"
And the domain classes should contain "uko-infra:Port"
And the domain classes should contain "uko-infra:ConfigBlock"
And the domain classes should contain "uko-infra:ConfigKey"
@ontology @registry @negative
Scenario: Look up unknown domain raises ValueError
When I look up domain "uko-xyz:"
Then a ValueError should have been raised with message "Unknown UKO domain prefix"
@ontology @registry @negative
Scenario: Look up empty domain prefix raises ValueError
When I look up an empty domain prefix
Then a ValueError should have been raised with message "must not be empty"
# ---------------------------------------------------------------------------
# List Domains
# ---------------------------------------------------------------------------
@ontology @registry
Scenario: list_domains returns all 5 registered domains
When I list all domains
Then the domain list should have 5 entries
And the domain list should be sorted by layer then prefix
@ontology @registry
Scenario: get_layer1_domains returns exactly 4 domains
When I get Layer 1 domains
Then the Layer 1 list should have 4 entries
And every domain in the Layer 1 list should have layer 1
# ---------------------------------------------------------------------------
# DetailLevelMap — spec-complete resolution for uko-code:
# ---------------------------------------------------------------------------
@ontology @detail_levels
Scenario: uko-code: map resolves all 10 named levels
When I look up domain "uko-code:"
Then the detail map should resolve "MODULE_LISTING" to 0
And the detail map should resolve "MODULE_GRAPH" to 1
And the detail map should resolve "MEMBER_LISTING" to 2
And the detail map should resolve "MEMBER_SUMMARY" to 3
And the detail map should resolve "SIGNATURES" to 4
And the detail map should resolve "SIGNATURES_WITH_DOCS" to 5
And the detail map should resolve "STRUCTURAL_OUTLINE" to 6
And the detail map should resolve "KEY_LOGIC" to 7
And the detail map should resolve "NEAR_COMPLETE" to 8
And the detail map should resolve "FULL_SOURCE" to 9
# ---------------------------------------------------------------------------
# DetailLevelMap — spec-complete resolution for uko-doc:
# ---------------------------------------------------------------------------
@ontology @detail_levels
Scenario: uko-doc: map resolves all 11 named levels
When I look up domain "uko-doc:"
Then the detail map should resolve "TITLE_ONLY" to 0
And the detail map should resolve "TABLE_OF_CONTENTS_L1" to 1
And the detail map should resolve "TABLE_OF_CONTENTS_L2" to 2
And the detail map should resolve "FULL_TOC" to 3
And the detail map should resolve "TOC_WITH_SUMMARIES" to 4
And the detail map should resolve "SECTION_OVERVIEWS" to 5
And the detail map should resolve "TOPIC_SENTENCES" to 6
And the detail map should resolve "PARAGRAPH_SUMMARIES" to 7
And the detail map should resolve "STRUCTURAL_DETAIL" to 8
And the detail map should resolve "NEAR_COMPLETE" to 9
And the detail map should resolve "FULL_CONTENT" to 10
# ---------------------------------------------------------------------------
# DetailLevelMap — spec-complete resolution for uko-data:
# ---------------------------------------------------------------------------
@ontology @detail_levels
Scenario: uko-data: map resolves all 12 named levels
When I look up domain "uko-data:"
Then the detail map should resolve "SCHEMA_LISTING" to 0
And the detail map should resolve "TABLE_LISTING" to 1
And the detail map should resolve "COLUMN_LISTING" to 2
And the detail map should resolve "TYPED_COLUMNS" to 3
And the detail map should resolve "CONSTRAINTS" to 4
And the detail map should resolve "RELATIONSHIPS" to 5
And the detail map should resolve "INDEXES_AND_STATS" to 6
And the detail map should resolve "DDL" to 7
And the detail map should resolve "DDL_WITH_TRIGGERS" to 8
And the detail map should resolve "FULL_PROCEDURES" to 9
And the detail map should resolve "WITH_SAMPLE_DATA" to 10
And the detail map should resolve "FULL_CATALOG" to 11
# ---------------------------------------------------------------------------
# DetailLevelMap — spec-complete resolution for uko-infra:
# ---------------------------------------------------------------------------
@ontology @detail_levels
Scenario: uko-infra: map resolves all 9 named levels
When I look up domain "uko-infra:"
Then the detail map should resolve "SERVICE_LISTING" to 0
And the detail map should resolve "SERVICE_GRAPH" to 1
And the detail map should resolve "ENDPOINT_LISTING" to 2
And the detail map should resolve "ENDPOINT_DETAIL" to 3
And the detail map should resolve "CONFIG_KEYS" to 4
And the detail map should resolve "CONFIG_VALUES" to 5
And the detail map should resolve "RESOURCE_LIMITS" to 6
And the detail map should resolve "FULL_CONFIG" to 7
And the detail map should resolve "WITH_DEPLOYMENT" to 8
# ---------------------------------------------------------------------------
# DetailLevelMap — integer depth resolution
# ---------------------------------------------------------------------------
@ontology @detail_levels
Scenario: Integer depth is clamped to max_depth
When I look up domain "uko-code:"
Then the detail map should clamp depth 15 to 9
And the detail map should clamp depth 0 to 0
And the detail map should clamp depth 5 to 5
@ontology @detail_levels @negative
Scenario: Unknown named level raises ValueError
When I look up domain "uko-code:"
And I try to resolve level "NONEXISTENT_LEVEL"
Then a ValueError should have been raised with message "Unknown detail level"
# ---------------------------------------------------------------------------
# DetailLevelMap — inheritance chain (Layer 3 → 2 → 1 → 0)
# ---------------------------------------------------------------------------
@ontology @detail_levels @inheritance
Scenario: build_detail_map_chain wires parent linkage
When I build a detail map chain from "uko-code:"
Then the chain should resolve "MODULE_LISTING" to 0
And the chain should resolve "FULL_SOURCE" to 9
And the chain max depth should be 9
@ontology @detail_levels @inheritance
Scenario: Two-level chain inherits parent levels
Given a custom DetailLevelMap for domain "uko-oo:" with parent "uko-code:" and levels:
| level | depth |
| CLASS_HIERARCHY | 3 |
When I resolve "MODULE_LISTING" on the custom map
Then the resolved depth should be 0
When I resolve "CLASS_HIERARCHY" on the custom map
Then the resolved depth should be 3
@ontology @detail_levels @inheritance @negative
Scenario: build_detail_map_chain with no prefixes raises ValueError
When I build a detail map chain with no prefixes
Then a ValueError should have been raised with message "At least one domain prefix"
# ---------------------------------------------------------------------------
# Turtle Validation — well-formed file
# ---------------------------------------------------------------------------
@ontology @turtle
Scenario: The project uko.ttl file passes Turtle validation
When I validate the project Turtle file "docs/ontology/uko.ttl"
Then the validation should produce 0 errors
@ontology @turtle
Scenario: Valid minimal Turtle content passes validation
When I validate Turtle content:
"""
@prefix ex: <http://example.org/> .
ex:Thing a ex:Class .
"""
Then the validation should produce 0 errors
# ---------------------------------------------------------------------------
# Turtle Validation — error detection
# ---------------------------------------------------------------------------
@ontology @turtle @negative
Scenario: Undeclared prefix is caught
When I validate Turtle content:
"""
@prefix ex: <http://example.org/> .
bogus:Thing a ex:Class .
"""
Then the validation should produce at least 1 error
And the first error should contain "undeclared prefix"
@ontology @turtle @negative
Scenario: Unbalanced angle brackets are caught
When I validate Turtle content:
"""
@prefix ex: <http://example.org/> .
ex:Thing a <http://example.org/Class .
"""
Then the validation should produce at least 1 error
And the first error should contain "unbalanced angle brackets"
@ontology @turtle @negative
Scenario: Empty content is caught
When I validate empty Turtle content
Then the validation should produce at least 1 error
And the first error should contain "Empty"
@ontology @turtle @negative
Scenario: validate_turtle_file with missing file raises FileNotFoundError
When I validate Turtle file "/nonexistent/path/to/file.ttl"
Then a FileNotFoundError should have been raised
@ontology @turtle @negative
Scenario: validate_turtle_file with empty path raises ValueError
When I validate Turtle file with empty path
Then a ValueError should have been raised with message "must not be empty"
# ---------------------------------------------------------------------------
# Turtle Content — Layer 1 prefix declarations
# ---------------------------------------------------------------------------
@ontology @turtle
Scenario: The project uko.ttl declares all Layer 1 prefixes
When I read the project Turtle file "docs/ontology/uko.ttl"
Then the content should contain prefix "uko-code:"
And the content should contain prefix "uko-doc:"
And the content should contain prefix "uko-data:"
And the content should contain prefix "uko-infra:"
@ontology @turtle
Scenario: The project uko.ttl declares uko-doc: classes
When I read the project Turtle file "docs/ontology/uko.ttl"
Then the content should contain "uko-doc:Document"
And the content should contain "uko-doc:Section"
And the content should contain "uko-doc:Paragraph"
And the content should contain "uko-doc:Citation"
@ontology @turtle
Scenario: The project uko.ttl declares uko-data: classes
When I read the project Turtle file "docs/ontology/uko.ttl"
Then the content should contain "uko-data:Table"
And the content should contain "uko-data:Column"
And the content should contain "uko-data:ForeignKey"
And the content should contain "uko-data:View"
@ontology @turtle
Scenario: The project uko.ttl declares uko-infra: classes
When I read the project Turtle file "docs/ontology/uko.ttl"
Then the content should contain "uko-infra:Service"
And the content should contain "uko-infra:Endpoint"
And the content should contain "uko-infra:Port"
And the content should contain "uko-infra:ConfigBlock"
# ---------------------------------------------------------------------------
# Universal View Guarantee
# ---------------------------------------------------------------------------
@ontology @universal_view
Scenario: Every Layer 1 class ultimately inherits from uko:InformationUnit
When I read the project Turtle file "docs/ontology/uko.ttl"
Then every Layer 1 class should have a subClassOf chain to uko:InformationUnit
@ontology @universal_view
Scenario: All Layer 1 domain DetailLevelMaps include depth 0
When I get Layer 1 domains
Then every domain detail map should resolve integer depth 0 to 0
+167
View File
@@ -0,0 +1,167 @@
"""Robot Framework helper for UKO Ontology Registry integration smoke tests.
Exercises domain lookup, DetailLevelMap resolution across all four
Layer 1 domains, inheritance chain building, and Turtle validation
against the project ontology file.
Usage::
python robot/helper_uko_ontology_registry.py <command>
Commands
--------
domain_lookup
Look up each domain prefix and verify IRI / layer / class count.
detail_level_resolution
Resolve named levels for every Layer 1 domain.
inheritance_chain
Build a chain and resolve an inherited level.
turtle_validate
Validate ``docs/ontology/uko.ttl`` and verify zero errors.
universal_view
Verify all Layer 1 classes chain to ``uko:InformationUnit``.
list_all_domains
List all registered domains and print summary.
"""
from __future__ import annotations
import sys
from pathlib import Path
# Ensure src/ is on the path so domain models can be imported.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.domain.models.acms.ontology_registry import ( # noqa: E402
build_detail_map_chain,
get_domain,
get_layer1_domains,
list_domains,
validate_turtle_file,
)
_PROJECT_ROOT = Path(__file__).resolve().parents[1]
def _domain_lookup() -> int:
"""Look up every registered domain and print summary."""
expected = {
"uko:": ("https://cleveragents.ai/ontology/uko#", 0, 5),
"uko-code:": ("https://cleveragents.ai/ontology/uko/code#", 1, 5),
"uko-doc:": ("https://cleveragents.ai/ontology/uko/doc#", 1, 17),
"uko-data:": ("https://cleveragents.ai/ontology/uko/data#", 1, 13),
"uko-infra:": ("https://cleveragents.ai/ontology/uko/infra#", 1, 7),
}
for prefix, (iri, layer, cls_count) in expected.items():
desc = get_domain(prefix)
assert desc.iri == iri, f"{prefix}: IRI mismatch"
assert desc.layer == layer, f"{prefix}: layer mismatch"
assert len(desc.classes) == cls_count, (
f"{prefix}: expected {cls_count} classes, got {len(desc.classes)}"
)
print(f"OK {prefix} -> {iri} (layer={layer}, classes={cls_count})")
print("DOMAIN_LOOKUP_PASSED")
return 0
def _detail_level_resolution() -> int:
"""Resolve key levels for every Layer 1 domain."""
checks: list[tuple[str, str, int]] = [
("uko-code:", "MODULE_LISTING", 0),
("uko-code:", "FULL_SOURCE", 9),
("uko-code:", "SIGNATURES", 4),
("uko-doc:", "TITLE_ONLY", 0),
("uko-doc:", "FULL_CONTENT", 10),
("uko-doc:", "TOPIC_SENTENCES", 6),
("uko-data:", "SCHEMA_LISTING", 0),
("uko-data:", "FULL_CATALOG", 11),
("uko-data:", "TYPED_COLUMNS", 3),
("uko-infra:", "SERVICE_LISTING", 0),
("uko-infra:", "WITH_DEPLOYMENT", 8),
("uko-infra:", "CONFIG_KEYS", 4),
]
for prefix, level, expected in checks:
desc = get_domain(prefix)
actual = desc.detail_map.resolve(level)
assert actual == expected, (
f"{prefix}.resolve({level!r}) = {actual}, expected {expected}"
)
print(f"OK {prefix} {level} -> {actual}")
print("DETAIL_LEVEL_RESOLUTION_PASSED")
return 0
def _inheritance_chain() -> int:
"""Build a chain from uko-code: and verify inherited levels."""
chain = build_detail_map_chain("uko-code:")
assert chain.resolve("MODULE_LISTING") == 0
assert chain.resolve("FULL_SOURCE") == 9
assert chain.max_depth == 9
print("OK chain resolves inherited levels correctly")
print("INHERITANCE_CHAIN_PASSED")
return 0
def _turtle_validate() -> int:
"""Validate the project ontology file."""
ttl_path = _PROJECT_ROOT / "docs" / "ontology" / "uko.ttl"
errors = validate_turtle_file(ttl_path)
if errors:
for err in errors:
print(f"ERROR: {err}", file=sys.stderr)
return 1
print(f"OK {ttl_path} passed Turtle validation (0 errors)")
print("TURTLE_VALIDATE_PASSED")
return 0
def _universal_view() -> int:
"""Verify all Layer 1 domains have depth 0 resolving to 0."""
for desc in get_layer1_domains():
actual = desc.detail_map.resolve(0)
assert actual == 0, f"{desc.prefix} resolved depth 0 to {actual}, expected 0"
print(f"OK {desc.prefix} depth 0 -> 0")
print("UNIVERSAL_VIEW_PASSED")
return 0
def _list_all_domains() -> int:
"""List all registered domains with summary."""
domains = list_domains()
assert len(domains) == 5, f"Expected 5 domains, got {len(domains)}"
for d in domains:
levels = d.detail_map.effective_levels()
print(
f" {d.prefix:<14} layer={d.layer} "
f"classes={len(d.classes):<3} levels={len(levels)}"
)
print("LIST_ALL_DOMAINS_PASSED")
return 0
def main() -> int:
"""Dispatch to the requested command."""
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <command>", file=sys.stderr)
return 1
cmd = sys.argv[1]
dispatch = {
"domain_lookup": _domain_lookup,
"detail_level_resolution": _detail_level_resolution,
"inheritance_chain": _inheritance_chain,
"turtle_validate": _turtle_validate,
"universal_view": _universal_view,
"list_all_domains": _list_all_domains,
}
handler = dispatch.get(cmd)
if handler is None:
print(f"Unknown command: {cmd}", file=sys.stderr)
return 1
return handler()
if __name__ == "__main__":
sys.exit(main())
+66
View File
@@ -0,0 +1,66 @@
*** Settings ***
Documentation Integration smoke tests for UKO Ontology Domain Registry.
... Validates domain lookup, DetailLevelMap resolution across
... all four Layer 1 domains, inheritance chain building,
... Turtle syntax validation, and the Universal View Guarantee.
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_uko_ontology_registry.py
*** Test Cases ***
Domain Lookup Smoke Test
[Documentation] Look up every registered domain prefix and verify
... IRI, layer, and class count match expectations.
${result}= Run Process ${PYTHON} ${HELPER} domain_lookup cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} DOMAIN_LOOKUP_PASSED
Detail Level Resolution Smoke Test
[Documentation] Resolve key named levels for every Layer 1 domain
... and verify integer depths match the spec.
${result}= Run Process ${PYTHON} ${HELPER} detail_level_resolution cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} DETAIL_LEVEL_RESOLUTION_PASSED
Inheritance Chain Smoke Test
[Documentation] Build a DetailLevelMap chain and verify inherited
... level resolution works across the chain.
${result}= Run Process ${PYTHON} ${HELPER} inheritance_chain cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} INHERITANCE_CHAIN_PASSED
Turtle File Validation Smoke Test
[Documentation] Validate the project ontology file docs/ontology/uko.ttl
... passes lightweight Turtle syntax validation.
${result}= Run Process ${PYTHON} ${HELPER} turtle_validate cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} TURTLE_VALIDATE_PASSED
Universal View Guarantee Smoke Test
[Documentation] Verify all Layer 1 domains resolve depth 0 to 0,
... confirming the universal depth interface.
${result}= Run Process ${PYTHON} ${HELPER} universal_view cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} UNIVERSAL_VIEW_PASSED
List All Domains Smoke Test
[Documentation] List all registered domains and verify the count
... and structure are correct.
${result}= Run Process ${PYTHON} ${HELPER} list_all_domains cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} LIST_ALL_DOMAINS_PASSED
@@ -553,24 +553,37 @@ _AssembledContextLike = Any
def code_detail_map() -> DetailLevelMap:
"""Return the built-in source-code detail level map.
"""Return the built-in ``uko-code:`` detail level map.
Maps named levels to integer depths following the spec examples
(lines 25280-25304):
Authoritative source: ``docs/specification.md`` §41934-41971.
- ``MODULE_LISTING`` = 0
- ``CLASS_NAMES`` = 2
- ``SIGNATURES`` = 4
- ``MEMBER_SUMMARY`` = 6
- ``FULL_SOURCE`` = 9
=========================== =====
Named Level Depth
=========================== =====
``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
=========================== =====
"""
return DetailLevelMap(
domain="uko-code:",
levels={
"MODULE_LISTING": 0,
"CLASS_NAMES": 2,
"MODULE_GRAPH": 1,
"MEMBER_LISTING": 2,
"MEMBER_SUMMARY": 3,
"SIGNATURES": 4,
"MEMBER_SUMMARY": 6,
"SIGNATURES_WITH_DOCS": 5,
"STRUCTURAL_OUTLINE": 6,
"KEY_LOGIC": 7,
"NEAR_COMPLETE": 8,
"FULL_SOURCE": 9,
},
max_depth=9,
@@ -578,26 +591,39 @@ def code_detail_map() -> DetailLevelMap:
def docs_detail_map() -> DetailLevelMap:
"""Return the built-in documentation detail level map.
"""Return the built-in ``uko-doc:`` detail level map.
Maps named levels to integer depths following the spec examples
(lines 25310-25336):
Authoritative source: ``docs/specification.md`` §42023-42083.
- ``TITLE_ONLY`` = 0
- ``TABLE_OF_CONTENTS`` = 2
- ``SECTION_HEADINGS`` = 4
- ``TOPIC_SENTENCES`` = 6
- ``SECTION_SUMMARIES`` = 8
- ``FULL_CONTENT`` = 10
=========================== =====
Named Level Depth
=========================== =====
``TITLE_ONLY`` 0
``TABLE_OF_CONTENTS_L1`` 1
``TABLE_OF_CONTENTS_L2`` 2
``FULL_TOC`` 3
``TOC_WITH_SUMMARIES`` 4
``SECTION_OVERVIEWS`` 5
``TOPIC_SENTENCES`` 6
``PARAGRAPH_SUMMARIES`` 7
``STRUCTURAL_DETAIL`` 8
``NEAR_COMPLETE`` 9
``FULL_CONTENT`` 10
=========================== =====
"""
return DetailLevelMap(
domain="uko-doc:",
levels={
"TITLE_ONLY": 0,
"TABLE_OF_CONTENTS": 2,
"SECTION_HEADINGS": 4,
"TABLE_OF_CONTENTS_L1": 1,
"TABLE_OF_CONTENTS_L2": 2,
"FULL_TOC": 3,
"TOC_WITH_SUMMARIES": 4,
"SECTION_OVERVIEWS": 5,
"TOPIC_SENTENCES": 6,
"SECTION_SUMMARIES": 8,
"PARAGRAPH_SUMMARIES": 7,
"STRUCTURAL_DETAIL": 8,
"NEAR_COMPLETE": 9,
"FULL_CONTENT": 10,
},
max_depth=10,
@@ -605,22 +631,78 @@ def docs_detail_map() -> DetailLevelMap:
def database_detail_map() -> DetailLevelMap:
"""Return the built-in database detail level map.
"""Return the built-in ``uko-data:`` detail level map.
- ``TABLE_LISTING`` = 0
- ``COLUMN_NAMES`` = 3
- ``SCHEMA_DETAILS`` = 6
- ``INDEX_DETAILS`` = 9
- ``FULL_CATALOG`` = 11
Authoritative source: ``docs/specification.md`` §42167-42230.
=========================== =====
Named Level Depth
=========================== =====
``SCHEMA_LISTING`` 0
``TABLE_LISTING`` 1
``COLUMN_LISTING`` 2
``TYPED_COLUMNS`` 3
``CONSTRAINTS`` 4
``RELATIONSHIPS`` 5
``INDEXES_AND_STATS`` 6
``DDL`` 7
``DDL_WITH_TRIGGERS`` 8
``FULL_PROCEDURES`` 9
``WITH_SAMPLE_DATA`` 10
``FULL_CATALOG`` 11
=========================== =====
"""
return DetailLevelMap(
domain="uko-data:",
levels={
"TABLE_LISTING": 0,
"COLUMN_NAMES": 3,
"SCHEMA_DETAILS": 6,
"INDEX_DETAILS": 9,
"SCHEMA_LISTING": 0,
"TABLE_LISTING": 1,
"COLUMN_LISTING": 2,
"TYPED_COLUMNS": 3,
"CONSTRAINTS": 4,
"RELATIONSHIPS": 5,
"INDEXES_AND_STATS": 6,
"DDL": 7,
"DDL_WITH_TRIGGERS": 8,
"FULL_PROCEDURES": 9,
"WITH_SAMPLE_DATA": 10,
"FULL_CATALOG": 11,
},
max_depth=11,
)
def infra_detail_map() -> DetailLevelMap:
"""Return the built-in ``uko-infra:`` detail level map.
Authoritative source: ``docs/specification.md`` §42297-42331.
=========================== =====
Named Level Depth
=========================== =====
``SERVICE_LISTING`` 0
``SERVICE_GRAPH`` 1
``ENDPOINT_LISTING`` 2
``ENDPOINT_DETAIL`` 3
``CONFIG_KEYS`` 4
``CONFIG_VALUES`` 5
``RESOURCE_LIMITS`` 6
``FULL_CONFIG`` 7
``WITH_DEPLOYMENT`` 8
=========================== =====
"""
return DetailLevelMap(
domain="uko-infra:",
levels={
"SERVICE_LISTING": 0,
"SERVICE_GRAPH": 1,
"ENDPOINT_LISTING": 2,
"ENDPOINT_DETAIL": 3,
"CONFIG_KEYS": 4,
"CONFIG_VALUES": 5,
"RESOURCE_LIMITS": 6,
"FULL_CONFIG": 7,
"WITH_DEPLOYMENT": 8,
},
max_depth=8,
)
@@ -65,6 +65,12 @@ Concrete analyzers:
- ``PostgreSQLAnalyzer`` -- Regex-based DDL/SQL analyzer
- ``DockerComposeAnalyzer`` -- YAML-based Docker Compose analyzer
Ontology registry
(from :mod:`~cleveragents.domain.models.acms.ontology_registry`):
- ``DomainDescriptor`` -- Immutable metadata for a UKO domain
- ``OntologyRegistry`` functions -- Domain lookup, Layer 1 listing,
DetailLevelMap chain building, Turtle validation
Based on ``docs/specification.md`` ACMS / CRP sections and ADR-014.
"""
@@ -95,6 +101,16 @@ from cleveragents.domain.models.acms.docker_compose_analyzer import (
DockerComposeAnalyzer,
)
from cleveragents.domain.models.acms.markdown_analyzer import MarkdownAnalyzer
from cleveragents.domain.models.acms.ontology_registry import (
DomainDescriptor,
TurtleValidationError,
build_detail_map_chain,
get_domain,
get_layer1_domains,
list_domains,
validate_turtle,
validate_turtle_file,
)
from cleveragents.domain.models.acms.postgresql_analyzer import PostgreSQLAnalyzer
from cleveragents.domain.models.acms.python_analyzer import PythonAnalyzer
from cleveragents.domain.models.acms.scope_resolution import (
@@ -158,6 +174,7 @@ __all__: list[str] = [
"ContextTier",
"DetailLevelMap",
"DockerComposeAnalyzer",
"DomainDescriptor",
"FragmentProvenance",
"GraphBackend",
"GraphResult",
@@ -185,11 +202,18 @@ __all__: list[str] = [
"TierBudget",
"TierMetrics",
"TieredFragment",
"TurtleValidationError",
"UKOTriple",
"VectorBackend",
"VectorResult",
"build_detail_map_chain",
"create_scoped_backend_set",
"get_domain",
"get_layer1_domains",
"list_domains",
"resolve_resource_scope",
"validate_project_scope",
"validate_resource_scope",
"validate_turtle",
"validate_turtle_file",
]
@@ -0,0 +1,514 @@
"""UKO Ontology Domain Registry and Turtle syntax validation.
Central registry that binds each UKO Layer 1 domain to its namespace
IRI, OWL class inventory, and ``DetailLevelMap``. Provides Turtle
syntax validation for the project ontology file
(``docs/ontology/uko.ttl``).
## Registered Domains
| Prefix | Namespace IRI |
|---------------|----------------------------------------------------|
| ``uko:`` | ``https://cleveragents.ai/ontology/uko#`` |
| ``uko-code:`` | ``https://cleveragents.ai/ontology/uko/code#`` |
| ``uko-doc:`` | ``https://cleveragents.ai/ontology/uko/doc#`` |
| ``uko-data:`` | ``https://cleveragents.ai/ontology/uko/data#`` |
| ``uko-infra:``| ``https://cleveragents.ai/ontology/uko/infra#`` |
## Turtle Validation
``validate_turtle()`` performs a lightweight parse of Turtle files
without requiring ``rdflib``. It checks:
1. Every ``@prefix`` declaration has balanced syntax.
2. Every statement ends with ``.`` (or ``;`` / ``,`` for continuation).
3. All ``@prefix`` IRIs referenced in the body are declared.
4. Balanced angle brackets (``<…>``), quotes, and parentheses.
Based on ``docs/specification.md`` §41830-42332 (UKO Ontology Hierarchy).
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
from typing import NamedTuple
from cleveragents.domain.models.acms.crp import DetailLevelMap
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Constants — namespace IRIs (§41873-41892)
# ---------------------------------------------------------------------------
UKO_BASE_IRI: str = "https://cleveragents.ai/ontology/uko#"
"""Layer 0 universal foundation namespace."""
UKO_CODE_IRI: str = "https://cleveragents.ai/ontology/uko/code#"
"""Layer 1 general software namespace."""
UKO_DOC_IRI: str = "https://cleveragents.ai/ontology/uko/doc#"
"""Layer 1 documents namespace."""
UKO_DATA_IRI: str = "https://cleveragents.ai/ontology/uko/data#"
"""Layer 1 data schemas namespace."""
UKO_INFRA_IRI: str = "https://cleveragents.ai/ontology/uko/infra#"
"""Layer 1 infrastructure namespace."""
# ---------------------------------------------------------------------------
# Layer 0 class inventory
# ---------------------------------------------------------------------------
LAYER0_CLASSES: frozenset[str] = frozenset(
{
"uko:InformationUnit",
"uko:Container",
"uko:Atom",
"uko:Annotation",
"uko:Boundary",
}
)
"""Layer 0 base classes defined in the UKO ontology."""
# ---------------------------------------------------------------------------
# Layer 1 class inventories — from spec §41934-42331
# ---------------------------------------------------------------------------
LAYER1_CODE_CLASSES: frozenset[str] = frozenset(
{
"uko-code:Module",
"uko-code:Callable",
"uko-code:TypeDefinition",
"uko-code:TestCase",
"uko-code:Import",
}
)
"""``uko-code:`` classes for general software."""
LAYER1_DOC_CLASSES: frozenset[str] = frozenset(
{
# Containers
"uko-doc:Document",
"uko-doc:Part",
"uko-doc:Chapter",
"uko-doc:Section",
"uko-doc:Subsection",
# Atoms
"uko-doc:Paragraph",
"uko-doc:CodeBlock",
"uko-doc:Figure",
"uko-doc:Table",
"uko-doc:BlockQuote",
"uko-doc:ListBlock",
# Annotations
"uko-doc:Footnote",
"uko-doc:Citation",
"uko-doc:Comment",
# Boundaries
"uko-doc:TableOfContents",
"uko-doc:Index",
"uko-doc:Glossary",
}
)
"""``uko-doc:`` classes for documents."""
LAYER1_DATA_CLASSES: frozenset[str] = frozenset(
{
# Containers
"uko-data:Database",
"uko-data:Schema",
"uko-data:Table",
"uko-data:View",
# Atoms
"uko-data:Column",
"uko-data:StoredProcedure",
"uko-data:Trigger",
"uko-data:Migration",
# Annotations
"uko-data:Constraint",
"uko-data:Index",
"uko-data:ColumnComment",
# Boundaries
"uko-data:ForeignKey",
"uko-data:APIEndpoint",
}
)
"""``uko-data:`` classes for data schemas."""
LAYER1_INFRA_CLASSES: frozenset[str] = frozenset(
{
# Containers
"uko-infra:Service",
"uko-infra:ConfigBlock",
"uko-infra:DeploymentUnit",
# Atoms
"uko-infra:ConfigKey",
"uko-infra:EnvironmentVariable",
# Boundaries
"uko-infra:Endpoint",
"uko-infra:Port",
}
)
"""``uko-infra:`` classes for infrastructure."""
# ---------------------------------------------------------------------------
# DomainDescriptor — metadata for a UKO domain
# ---------------------------------------------------------------------------
class DomainDescriptor(NamedTuple):
"""Immutable metadata for a single UKO domain."""
prefix: str
"""Turtle prefix (e.g., ``"uko-code:"``)."""
iri: str
"""Full namespace IRI."""
layer: int
"""Ontology layer (0, 1, 2, or 3)."""
classes: frozenset[str]
"""Set of class QNames defined in this domain."""
detail_map: DetailLevelMap
"""The authoritative ``DetailLevelMap`` for this domain."""
# ---------------------------------------------------------------------------
# Authoritative DetailLevelMap definitions (spec §41934-42331)
# ---------------------------------------------------------------------------
# These are defined here in the domain layer (not in the application layer)
# because they are pure data declarations tied to the ontology spec,
# not application-level services. The application-layer preset functions
# in depth_breadth_projection.py delegate to these same values.
_CODE_LEVELS: dict[str, int] = {
"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,
}
_DOC_LEVELS: dict[str, int] = {
"TITLE_ONLY": 0,
"TABLE_OF_CONTENTS_L1": 1,
"TABLE_OF_CONTENTS_L2": 2,
"FULL_TOC": 3,
"TOC_WITH_SUMMARIES": 4,
"SECTION_OVERVIEWS": 5,
"TOPIC_SENTENCES": 6,
"PARAGRAPH_SUMMARIES": 7,
"STRUCTURAL_DETAIL": 8,
"NEAR_COMPLETE": 9,
"FULL_CONTENT": 10,
}
_DATA_LEVELS: dict[str, int] = {
"SCHEMA_LISTING": 0,
"TABLE_LISTING": 1,
"COLUMN_LISTING": 2,
"TYPED_COLUMNS": 3,
"CONSTRAINTS": 4,
"RELATIONSHIPS": 5,
"INDEXES_AND_STATS": 6,
"DDL": 7,
"DDL_WITH_TRIGGERS": 8,
"FULL_PROCEDURES": 9,
"WITH_SAMPLE_DATA": 10,
"FULL_CATALOG": 11,
}
_INFRA_LEVELS: dict[str, int] = {
"SERVICE_LISTING": 0,
"SERVICE_GRAPH": 1,
"ENDPOINT_LISTING": 2,
"ENDPOINT_DETAIL": 3,
"CONFIG_KEYS": 4,
"CONFIG_VALUES": 5,
"RESOURCE_LIMITS": 6,
"FULL_CONFIG": 7,
"WITH_DEPLOYMENT": 8,
}
# ---------------------------------------------------------------------------
# OntologyRegistry
# ---------------------------------------------------------------------------
_PRESETS_LOADED: bool = False
_DOMAINS: dict[str, DomainDescriptor] = {}
def _ensure_presets() -> None:
"""Lazily build the domain registry on first access."""
global _PRESETS_LOADED
if _PRESETS_LOADED:
return
_DOMAINS.update(
{
"uko:": DomainDescriptor(
prefix="uko:",
iri=UKO_BASE_IRI,
layer=0,
classes=LAYER0_CLASSES,
detail_map=DetailLevelMap(
domain="uko:",
levels={},
max_depth=0,
),
),
"uko-code:": DomainDescriptor(
prefix="uko-code:",
iri=UKO_CODE_IRI,
layer=1,
classes=LAYER1_CODE_CLASSES,
detail_map=DetailLevelMap(
domain="uko-code:",
levels=dict(_CODE_LEVELS),
max_depth=9,
),
),
"uko-doc:": DomainDescriptor(
prefix="uko-doc:",
iri=UKO_DOC_IRI,
layer=1,
classes=LAYER1_DOC_CLASSES,
detail_map=DetailLevelMap(
domain="uko-doc:",
levels=dict(_DOC_LEVELS),
max_depth=10,
),
),
"uko-data:": DomainDescriptor(
prefix="uko-data:",
iri=UKO_DATA_IRI,
layer=1,
classes=LAYER1_DATA_CLASSES,
detail_map=DetailLevelMap(
domain="uko-data:",
levels=dict(_DATA_LEVELS),
max_depth=11,
),
),
"uko-infra:": DomainDescriptor(
prefix="uko-infra:",
iri=UKO_INFRA_IRI,
layer=1,
classes=LAYER1_INFRA_CLASSES,
detail_map=DetailLevelMap(
domain="uko-infra:",
levels=dict(_INFRA_LEVELS),
max_depth=8,
),
),
}
)
_PRESETS_LOADED = True
def get_domain(prefix: str) -> DomainDescriptor:
"""Look up a domain descriptor by prefix.
Args:
prefix: UKO prefix (e.g., ``"uko-code:"``).
Returns:
The matching :class:`DomainDescriptor`.
Raises:
ValueError: If *prefix* is empty or unknown.
"""
if not prefix:
raise ValueError("Domain prefix must not be empty")
_ensure_presets()
desc = _DOMAINS.get(prefix)
if desc is None:
raise ValueError(f"Unknown UKO domain prefix: {prefix!r}")
return desc
def list_domains() -> list[DomainDescriptor]:
"""Return all registered domain descriptors, sorted by layer then prefix."""
_ensure_presets()
return sorted(_DOMAINS.values(), key=lambda d: (d.layer, d.prefix))
def get_layer1_domains() -> list[DomainDescriptor]:
"""Return only Layer 1 domain descriptors."""
return [d for d in list_domains() if d.layer == 1]
def build_detail_map_chain(
*prefixes: str,
) -> DetailLevelMap:
"""Build a chained ``DetailLevelMap`` from most-general to most-specific.
The inheritance chain is constructed so that the first prefix
is the root (no parent) and each subsequent prefix's map has the
previous one as its parent. This supports the Layer 3 → 2 → 1 → 0
resolution described in the spec.
Example::
chain = build_detail_map_chain("uko-code:", "uko-oo:", "uko-py:")
chain.resolve("MODULE_LISTING") # inherited from uko-code:
Args:
prefixes: One or more domain prefixes from most-general to
most-specific.
Returns:
The innermost (most-specific) ``DetailLevelMap`` with its
full inheritance chain wired.
Raises:
ValueError: If any prefix is unknown or no prefixes given.
"""
if not prefixes:
raise ValueError("At least one domain prefix is required")
current: DetailLevelMap | None = None
for pfx in prefixes:
desc = get_domain(pfx)
current = DetailLevelMap(
domain=desc.prefix,
parent=current,
levels=dict(desc.detail_map.levels),
max_depth=desc.detail_map.max_depth,
)
assert current is not None # at least one iteration guaranteed
return current
# ---------------------------------------------------------------------------
# Turtle syntax validation
# ---------------------------------------------------------------------------
# Regex for @prefix declarations: @prefix <name>: <iri> .
_PREFIX_RE = re.compile(
r"@prefix\s+([a-zA-Z][a-zA-Z0-9_-]*)\s*:\s*<([^>]+)>\s*\.",
)
# Regex for prefixed names used in the body (e.g., uko-code:Module)
_PREFIXED_NAME_RE = re.compile(
r"\b([a-zA-Z][a-zA-Z0-9_-]*):([a-zA-Z_]\w*)\b",
)
class TurtleValidationError(Exception):
"""Raised when Turtle syntax validation fails."""
def __init__(self, errors: list[str]) -> None:
self.errors = errors
super().__init__(f"Turtle validation failed with {len(errors)} error(s)")
def validate_turtle(content: str) -> list[str]:
"""Perform lightweight Turtle syntax validation.
Returns a list of error strings (empty if valid). This is NOT a
full OWL reasoner — it catches common structural issues:
1. All ``@prefix`` declarations are well-formed.
2. Every prefixed name (e.g., ``uko-code:Module``) references a
declared prefix.
3. Angle brackets ``<…>`` are balanced on each line.
4. Every top-level statement ends with ``.``.
Args:
content: Turtle file content as a string.
Returns:
List of error messages (empty means valid).
"""
if not content or not content.strip():
return ["Empty Turtle content"]
errors: list[str] = []
lines = content.splitlines()
# 1. Collect declared prefixes
declared_prefixes: set[str] = set()
for match in _PREFIX_RE.finditer(content):
declared_prefixes.add(match.group(1))
# Also accept standard prefixes that may not be declared but are
# used in Turtle (rdf, rdfs, owl, xsd are universal).
declared_prefixes.update({"rdf", "rdfs", "owl", "xsd"})
# 2. Check that all prefixed names reference declared prefixes
for lineno, line in enumerate(lines, 1):
stripped = line.strip()
if stripped.startswith("#") or not stripped:
continue
for m in _PREFIXED_NAME_RE.finditer(stripped):
pfx = m.group(1)
if pfx not in declared_prefixes:
errors.append(
f"Line {lineno}: undeclared prefix '{pfx}:' in '{m.group(0)}'"
)
# 3. Check angle bracket balance per line (ignoring comments)
for lineno, line in enumerate(lines, 1):
stripped = line.strip()
if stripped.startswith("#") or not stripped:
continue
# Strip string literals to avoid false positives
no_strings = re.sub(r'"[^"]*"', "", stripped)
opens = no_strings.count("<")
closes = no_strings.count(">")
if opens != closes:
errors.append(
f"Line {lineno}: unbalanced angle brackets "
f"({opens} open, {closes} close)"
)
# 4. Verify statements end with '.' (Turtle requirement)
# Accumulate non-comment, non-blank, non-prefix lines and check
# that the content ends with statements terminated by '.'.
body_lines: list[str] = []
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith("#") or stripped.startswith("@prefix"):
continue
body_lines.append(stripped)
if body_lines:
# The last non-blank body line should end with '.'
last = body_lines[-1]
if not last.endswith("."):
errors.append(
f"Final statement does not end with '.': '{last[:60]}...'"
if len(last) > 60
else f"Final statement does not end with '.': '{last}'"
)
return errors
def validate_turtle_file(path: str | Path) -> list[str]:
"""Validate a Turtle file on disk.
Args:
path: Path to the ``.ttl`` file.
Returns:
List of error messages (empty means valid).
Raises:
FileNotFoundError: If *path* does not exist.
ValueError: If *path* is empty.
"""
if not path:
raise ValueError("Path must not be empty")
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"Turtle file not found: {p}")
content = p.read_text(encoding="utf-8")
return validate_turtle(content)
+23
View File
@@ -833,3 +833,26 @@ facade # noqa: B018, F821
# Domain-specific analyzers — public API (issue #588)
PostgreSQLAnalyzer # noqa: B018, F821
DockerComposeAnalyzer # noqa: B018, F821
# UKO Ontology Domain Registry — public API (issue #574)
DomainDescriptor # noqa: B018, F821
TurtleValidationError # noqa: B018, F821
build_detail_map_chain # noqa: B018, F821
get_domain # noqa: B018, F821
get_layer1_domains # noqa: B018, F821
list_domains # noqa: B018, F821
validate_turtle # noqa: B018, F821
validate_turtle_file # noqa: B018, F821
infra_detail_map # noqa: B018, F821
# UKO Ontology Registry — constants and class inventories (issue #574)
UKO_BASE_IRI # noqa: B018, F821
UKO_CODE_IRI # noqa: B018, F821
UKO_DOC_IRI # noqa: B018, F821
UKO_DATA_IRI # noqa: B018, F821
UKO_INFRA_IRI # noqa: B018, F821
LAYER0_CLASSES # noqa: B018, F821
LAYER1_CODE_CLASSES # noqa: B018, F821
LAYER1_DOC_CLASSES # noqa: B018, F821
LAYER1_DATA_CLASSES # noqa: B018, F821
LAYER1_INFRA_CLASSES # noqa: B018, F821