77db78d768
Add Phase 2 domain-specific analyzers for the ACMS UKO indexing pipeline: - PostgreSQLAnalyzer: regex-based DDL parser extracting uko-data:Table, Column, ForeignKey, View, and Schema triples with column metadata (data type, nullability, primary key constraints). - DockerComposeAnalyzer: YAML-based parser extracting uko-infra: DeploymentUnit, Service, Port, EnvironmentVariable, and connectsTo triples. Validates Compose format via services/version key detection. Both satisfy AnalyzerProtocol and register in AnalyzerRegistry by file extension (.sql/.ddl and .yml/.yaml respectively). Includes 34 Behave BDD scenarios (protocol conformance, registry ops, triple extraction for all 4 analyzers, error handling, cross-analyzer URI scheme and confidence checks), 6 Robot Framework integration smoke tests, updated __init__.py exports, vulture whitelist, and CHANGELOG.
438 lines
12 KiB
Python
438 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Robot Framework helper for domain analyzer integration tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
# Ensure worktree src is *first* on sys.path so it shadows any installed copy.
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC in sys.path:
|
|
sys.path.remove(_SRC)
|
|
sys.path.insert(0, _SRC)
|
|
# Clear cached cleveragents modules so our worktree src is used.
|
|
for _key in list(sys.modules):
|
|
if _key.startswith("cleveragents"):
|
|
del sys.modules[_key]
|
|
|
|
from cleveragents.domain.models.acms.analyzers import ( # noqa: E402
|
|
AnalyzerProtocol,
|
|
AnalyzerRegistry,
|
|
)
|
|
from cleveragents.domain.models.acms.docker_compose_analyzer import ( # noqa: E402
|
|
DockerComposeAnalyzer,
|
|
)
|
|
from cleveragents.domain.models.acms.markdown_analyzer import ( # noqa: E402
|
|
MarkdownAnalyzer,
|
|
)
|
|
from cleveragents.domain.models.acms.postgresql_analyzer import ( # noqa: E402
|
|
PostgreSQLAnalyzer,
|
|
)
|
|
from cleveragents.domain.models.acms.python_analyzer import ( # noqa: E402
|
|
PythonAnalyzer,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sample content
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_SAMPLE_PYTHON = '''\
|
|
"""User authentication service for the application."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import logging
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class Credentials:
|
|
"""Holds user login credentials."""
|
|
|
|
username: str
|
|
password_hash: str
|
|
is_active: bool = True
|
|
roles: list[str] = field(default_factory=list)
|
|
|
|
|
|
class AuthenticationService:
|
|
"""Service responsible for authenticating users against the store."""
|
|
|
|
def __init__(self, secret_key: str) -> None:
|
|
self._secret_key = secret_key
|
|
self._sessions: dict[str, Credentials] = {}
|
|
|
|
def authenticate(self, username: str, password: str) -> Optional[str]:
|
|
"""Authenticate a user and return a session token.
|
|
|
|
Args:
|
|
username: The login name.
|
|
password: The plaintext password to verify.
|
|
|
|
Returns:
|
|
A session token string on success, or None on failure.
|
|
"""
|
|
pw_hash = hashlib.sha256(
|
|
(password + self._secret_key).encode()
|
|
).hexdigest()
|
|
logger.info("Authentication attempt for user=%s", username)
|
|
return pw_hash if username else None
|
|
|
|
def revoke_session(self, token: str) -> bool:
|
|
"""Revoke an active session by its token."""
|
|
if token in self._sessions:
|
|
del self._sessions[token]
|
|
return True
|
|
return False
|
|
|
|
|
|
def create_default_service() -> AuthenticationService:
|
|
"""Factory function to create a service with default settings."""
|
|
return AuthenticationService(secret_key="default-dev-key")
|
|
'''
|
|
|
|
_SAMPLE_MARKDOWN = """\
|
|
# Architecture Overview
|
|
|
|
This document describes the high-level architecture of our platform.
|
|
|
|
## Core Components
|
|
|
|
The system is composed of several key services:
|
|
|
|
- **API Gateway** — routes external traffic
|
|
- **Auth Service** — handles user authentication
|
|
- **Data Pipeline** — processes incoming events
|
|
|
|
For more details see [the design doc](https://wiki.example.com/design).
|
|
|
|
### API Gateway
|
|
|
|
The gateway is built on [Envoy](https://envoyproxy.io) and handles
|
|
TLS termination, rate limiting, and request routing.
|
|
|
|
```yaml
|
|
listeners:
|
|
- address: 0.0.0.0
|
|
port: 8443
|
|
tls: true
|
|
```
|
|
|
|
### Auth Service
|
|
|
|
Token-based authentication using JWT. See [RFC 7519](https://tools.ietf.org/html/rfc7519).
|
|
|
|
```python
|
|
def verify_token(token: str) -> Claims:
|
|
return jwt.decode(token, key=SECRET)
|
|
```
|
|
|
|
## Deployment
|
|
|
|
The platform runs on Kubernetes with Helm charts managed in the
|
|
`infra/` directory. Refer to [ops runbook](./runbook.md) for details.
|
|
"""
|
|
|
|
_SAMPLE_POSTGRESQL = """\
|
|
CREATE SCHEMA IF NOT EXISTS app;
|
|
|
|
CREATE TABLE app.users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(120) NOT NULL,
|
|
email VARCHAR(255) NOT NULL,
|
|
password VARCHAR(255) NOT NULL,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE app.roles (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(80) NOT NULL,
|
|
description TEXT
|
|
);
|
|
|
|
CREATE TABLE app.user_roles (
|
|
user_id INTEGER NOT NULL,
|
|
role_id INTEGER NOT NULL,
|
|
granted_at TIMESTAMP DEFAULT NOW(),
|
|
PRIMARY KEY (user_id, role_id),
|
|
FOREIGN KEY (user_id) REFERENCES app.users (id),
|
|
FOREIGN KEY (role_id) REFERENCES app.roles (id)
|
|
);
|
|
|
|
CREATE OR REPLACE VIEW app.active_users AS
|
|
SELECT u.id, u.username, u.email
|
|
FROM app.users u
|
|
WHERE u.is_active = TRUE;
|
|
"""
|
|
|
|
_SAMPLE_DOCKER_COMPOSE = """\
|
|
services:
|
|
web:
|
|
image: myapp/web:latest
|
|
ports:
|
|
- "8080:80"
|
|
- "8443:443"
|
|
environment:
|
|
DATABASE_URL: postgres://db:5432/app
|
|
REDIS_URL: redis://cache:6379
|
|
LOG_LEVEL: info
|
|
depends_on:
|
|
- db
|
|
- cache
|
|
volumes:
|
|
- ./config:/app/config:ro
|
|
|
|
db:
|
|
image: postgres:16-alpine
|
|
ports:
|
|
- "5432:5432"
|
|
environment:
|
|
POSTGRES_DB: app
|
|
POSTGRES_USER: appuser
|
|
POSTGRES_PASSWORD: secret
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
|
|
cache:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- "6379:6379"
|
|
|
|
worker:
|
|
image: myapp/worker:latest
|
|
environment:
|
|
DATABASE_URL: postgres://db:5432/app
|
|
REDIS_URL: redis://cache:6379
|
|
depends_on:
|
|
- db
|
|
- cache
|
|
"""
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper utilities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _has_object_uri(triples: list, uri_fragment: str) -> bool:
|
|
"""Return True if any triple has *uri_fragment* in its object_uri."""
|
|
return any(uri_fragment in t.object_uri for t in triples)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Commands
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _cmd_analyze_python() -> int:
|
|
"""Create a PythonAnalyzer, analyze sample Python, verify triple types."""
|
|
analyzer = PythonAnalyzer()
|
|
triples = analyzer.analyze(_SAMPLE_PYTHON, "src/auth/service.py")
|
|
|
|
if not triples:
|
|
print("analyze-python-fail: no triples produced")
|
|
return 1
|
|
|
|
has_module = _has_object_uri(triples, "uko-code:Module")
|
|
has_class = _has_object_uri(triples, "uko-py:Class")
|
|
has_function = _has_object_uri(triples, "uko-py:Function")
|
|
|
|
if not has_module:
|
|
print("analyze-python-fail: missing Module type")
|
|
return 1
|
|
if not has_class:
|
|
print("analyze-python-fail: missing Class type")
|
|
return 1
|
|
if not has_function:
|
|
print("analyze-python-fail: missing Function type")
|
|
return 1
|
|
|
|
print(f"analyze-python-ok: {len(triples)} triples (Module+Class+Function found)")
|
|
return 0
|
|
|
|
|
|
def _cmd_analyze_markdown() -> int:
|
|
"""Create a MarkdownAnalyzer, analyze sample markdown, verify triple types."""
|
|
analyzer = MarkdownAnalyzer()
|
|
triples = analyzer.analyze(_SAMPLE_MARKDOWN, "docs/architecture.md")
|
|
|
|
if not triples:
|
|
print("analyze-markdown-fail: no triples produced")
|
|
return 1
|
|
|
|
has_document = _has_object_uri(triples, "uko-doc:Document")
|
|
has_section = _has_object_uri(triples, "uko-doc:Section")
|
|
|
|
if not has_document:
|
|
print("analyze-markdown-fail: missing Document type")
|
|
return 1
|
|
if not has_section:
|
|
print("analyze-markdown-fail: missing Section type")
|
|
return 1
|
|
|
|
print(f"analyze-markdown-ok: {len(triples)} triples (Document+Section found)")
|
|
return 0
|
|
|
|
|
|
def _cmd_analyze_postgresql() -> int:
|
|
"""Create a PostgreSQLAnalyzer, analyze sample DDL, verify triple types."""
|
|
analyzer = PostgreSQLAnalyzer()
|
|
triples = analyzer.analyze(_SAMPLE_POSTGRESQL, "db/migrations/001_init.sql")
|
|
|
|
if not triples:
|
|
print("analyze-postgresql-fail: no triples produced")
|
|
return 1
|
|
|
|
has_table = _has_object_uri(triples, "uko-data:Table")
|
|
has_column = _has_object_uri(triples, "uko-data:Column")
|
|
|
|
if not has_table:
|
|
print("analyze-postgresql-fail: missing Table type")
|
|
return 1
|
|
if not has_column:
|
|
print("analyze-postgresql-fail: missing Column type")
|
|
return 1
|
|
|
|
print(f"analyze-postgresql-ok: {len(triples)} triples (Table+Column found)")
|
|
return 0
|
|
|
|
|
|
def _cmd_analyze_docker_compose() -> int:
|
|
"""Analyze sample compose YAML, verify triple types."""
|
|
analyzer = DockerComposeAnalyzer()
|
|
triples = analyzer.analyze(_SAMPLE_DOCKER_COMPOSE, "infra/docker-compose.yml")
|
|
|
|
if not triples:
|
|
print("analyze-docker-compose-fail: no triples produced")
|
|
return 1
|
|
|
|
has_service = _has_object_uri(triples, "uko-infra:Service")
|
|
has_deployment = _has_object_uri(triples, "uko-infra:DeploymentUnit")
|
|
|
|
if not has_service:
|
|
print("analyze-docker-compose-fail: missing Service type")
|
|
return 1
|
|
if not has_deployment:
|
|
print("analyze-docker-compose-fail: missing DeploymentUnit type")
|
|
return 1
|
|
|
|
print(
|
|
f"analyze-docker-compose-ok: {len(triples)} triples"
|
|
" (Service+DeploymentUnit found)"
|
|
)
|
|
return 0
|
|
|
|
|
|
def _cmd_protocol_check() -> int:
|
|
"""Verify all 4 analyzers satisfy AnalyzerProtocol."""
|
|
analyzer_classes = [
|
|
PythonAnalyzer,
|
|
MarkdownAnalyzer,
|
|
PostgreSQLAnalyzer,
|
|
DockerComposeAnalyzer,
|
|
]
|
|
|
|
for cls in analyzer_classes:
|
|
inst = cls()
|
|
if not isinstance(inst, AnalyzerProtocol):
|
|
print(
|
|
f"protocol-check-fail: {cls.__name__} doesn't satisfy AnalyzerProtocol"
|
|
)
|
|
return 1
|
|
if not inst.domain:
|
|
print(f"protocol-check-fail: {cls.__name__} has empty domain")
|
|
return 1
|
|
if not inst.supported_extensions:
|
|
print(f"protocol-check-fail: {cls.__name__} has no supported extensions")
|
|
return 1
|
|
print(
|
|
f"protocol-check: {cls.__name__} ok"
|
|
f" (domain={inst.domain}, extensions={sorted(inst.supported_extensions)})"
|
|
)
|
|
|
|
print("protocol-check-ok")
|
|
return 0
|
|
|
|
|
|
def _cmd_registry_check() -> int:
|
|
"""Create AnalyzerRegistry, register all 4 analyzers, verify lookup by extension."""
|
|
registry = AnalyzerRegistry()
|
|
analyzers = [
|
|
PythonAnalyzer(),
|
|
MarkdownAnalyzer(),
|
|
PostgreSQLAnalyzer(),
|
|
DockerComposeAnalyzer(),
|
|
]
|
|
|
|
for analyzer in analyzers:
|
|
registry.register(analyzer)
|
|
|
|
if len(registry) != 4:
|
|
print(f"registry-check-fail: expected 4 analyzers, got {len(registry)}")
|
|
return 1
|
|
|
|
# Verify extension lookup for each analyzer
|
|
extension_checks = {
|
|
".py": "python",
|
|
".pyi": "python",
|
|
".md": "markdown",
|
|
".markdown": "markdown",
|
|
".sql": "postgresql",
|
|
".ddl": "postgresql",
|
|
".yml": "docker-compose",
|
|
".yaml": "docker-compose",
|
|
}
|
|
|
|
for ext, expected_domain in extension_checks.items():
|
|
found = registry.get_for_extension(ext)
|
|
if found is None:
|
|
print(f"registry-check-fail: no analyzer found for '{ext}'")
|
|
return 1
|
|
if found.domain != expected_domain:
|
|
print(
|
|
f"registry-check-fail: expected domain '{expected_domain}'"
|
|
f" for '{ext}', got '{found.domain}'"
|
|
)
|
|
return 1
|
|
print(f"registry-check: '{ext}' -> {found.domain}")
|
|
|
|
# Verify unknown extension returns None
|
|
if registry.get_for_extension(".unknown") is not None:
|
|
print("registry-check-fail: expected None for unknown extension")
|
|
return 1
|
|
|
|
print(f"registry-check-ok: {len(registry)} analyzers, all extensions verified")
|
|
return 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_COMMANDS: dict[str, Callable[[], int]] = {
|
|
"analyze-python": _cmd_analyze_python,
|
|
"analyze-markdown": _cmd_analyze_markdown,
|
|
"analyze-postgresql": _cmd_analyze_postgresql,
|
|
"analyze-docker-compose": _cmd_analyze_docker_compose,
|
|
"protocol-check": _cmd_protocol_check,
|
|
"registry-check": _cmd_registry_check,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
"""Run the specified command."""
|
|
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
|
|
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>")
|
|
return 2
|
|
return _COMMANDS[sys.argv[1]]()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|