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
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:
@@ -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
@@ -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:)
|
||||
# ===========================================================================
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -188,7 +188,7 @@ Feature: UKO Ontology Loader
|
||||
Examples:
|
||||
| layer | count |
|
||||
| 0 | 18 |
|
||||
| 1 | 8 |
|
||||
| 1 | 67 |
|
||||
| 2 | 6 |
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user