Files
dss/dss-claude-plugin/core/context.py
DSS 9dbd56271e
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
feat: Enterprise DSS architecture implementation
Complete implementation of enterprise design system validation:

Phase 1 - @dss/rules npm package:
- CLI with validate and init commands
- 16 rules across 5 categories (colors, spacing, typography, components, a11y)
- dss-ignore support (inline and next-line)
- Break-glass [dss-skip] for emergency merges
- CI workflow templates (Gitea, GitHub, GitLab)

Phase 2 - Metrics dashboard:
- FastAPI metrics API with SQLite storage
- Portfolio-wide metrics aggregation
- Project drill-down with file:line:column violations
- Trend charts and history tracking

Phase 3 - Local analysis cache:
- LocalAnalysisCache for offline-capable validation
- Mode detection (LOCAL/REMOTE/CI)
- Stale cache warnings with recommendations

Phase 4 - Project onboarding:
- dss-init command for project setup
- Creates ds.config.json, .dss/ folder structure
- Updates .gitignore and package.json scripts
- Optional CI workflow setup

Architecture decisions:
- No commit-back: CI uploads to dashboard, not git
- Three-tier: Dashboard (read-only) → CI (authoritative) → Local (advisory)
- Pull-based rules via npm for version control

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-11 09:41:36 -03:00

313 lines
10 KiB
Python

"""
DSS Context Module
==================
Singleton context manager for the DSS Plugin.
Handles configuration loading, mode detection, and strategy instantiation.
Enterprise Architecture:
- LOCAL: Uses LocalAnalysisCache for fast, offline-capable validation
- REMOTE: Full analysis via API
- CI: Authoritative enforcement, uploads metrics to dashboard
"""
import asyncio
import logging
from typing import Any, Dict, Optional
from .config import DSSConfig, DSSMode
from .local_cache import LocalAnalysisCache, LocalCacheValidator, get_project_cache
# Logger setup
logger = logging.getLogger(__name__)
# Protocol/Type placeholder for Strategies (to be replaced by base class in next steps)
Strategy = Any
class DSSContext:
"""
Singleton context manager for the DSS Plugin.
Handles configuration loading, mode detection (Local/Remote),
and strategy instantiation.
"""
_instance: Optional["DSSContext"] = None
_lock: asyncio.Lock = asyncio.Lock()
def __init__(self) -> None:
"""
Private initializer.
Use get_instance() instead.
"""
if DSSContext._instance is not None:
raise RuntimeError("DSSContext is a singleton. Use get_instance() to access it.")
self.config: Optional[DSSConfig] = None
self.active_mode: DSSMode = DSSMode.REMOTE # Default safe fallback
self._capabilities: Dict[str, bool] = {}
self._strategy_cache: Dict[str, Strategy] = {}
self.session_id: Optional[str] = None
self._local_cache: Optional[LocalAnalysisCache] = None
self._cache_validator: Optional[LocalCacheValidator] = None
@classmethod
async def get_instance(cls) -> "DSSContext":
"""
Async factory method to get the singleton instance.
Ensures config is loaded and mode is detected before returning.
"""
if not cls._instance:
async with cls._lock:
# Double-check locking pattern
if not cls._instance:
instance = cls()
await instance._initialize()
cls._instance = instance
return cls._instance
@classmethod
def reset(cls) -> None:
"""
Resets the singleton instance.
Useful for testing.
"""
cls._instance = None
async def _initialize(self) -> None:
"""
Internal initialization logic:
1. Load Config
2. Detect Mode
3. Cache Capabilities
"""
try:
# 1. Load Configuration
self.config = DSSConfig.load()
self.session_id = self.config.session_id
# 2. Detect Mode (Async check)
self.active_mode = await self.config.get_active_mode()
logger.info(
f"DSSContext initialized. Mode: {self.active_mode.value}, Session: {self.session_id}"
)
# 3. Initialize local cache for LOCAL mode
if self.active_mode == DSSMode.LOCAL:
self._init_local_cache()
# 4. Cache Capabilities
self._cache_capabilities()
except Exception as e:
logger.error(f"Failed to initialize DSSContext: {e}")
# Fallback to defaults if initialization fails
self.active_mode = DSSMode.REMOTE
self._capabilities = {"limited": True}
def _init_local_cache(self) -> None:
"""Initialize local cache for LOCAL mode."""
try:
project_path = self.config.project_path if self.config else None
self._local_cache = get_project_cache(project_path)
self._cache_validator = LocalCacheValidator(self._local_cache)
# Log cache status
status = self._local_cache.get_cache_status()
if status.get("exists"):
if status.get("is_stale"):
logger.warning(f"Local cache is stale: {status.get('recommendation')}")
else:
logger.info(f"Local cache ready. Rules version: {status.get('rules_version')}")
else:
logger.info("No local cache found. Run `npx dss-rules validate` to populate.")
except Exception as e:
logger.warning(f"Failed to initialize local cache: {e}")
self._local_cache = None
self._cache_validator = None
def _cache_capabilities(self) -> None:
"""Determines what the plugin can do based on the active mode."""
# Base capabilities
caps = {
"can_read_files": False,
"can_execute_browser": False,
"can_screenshot": False,
"can_connect_remote": True,
}
if self.active_mode == DSSMode.LOCAL:
# Local mode allows direct filesystem access and local browser control
caps["can_read_files"] = True
caps["can_execute_browser"] = True
caps["can_screenshot"] = True
elif self.active_mode == DSSMode.REMOTE:
# Remote mode relies on API capabilities
# Depending on remote configuration, these might differ
caps[
"can_execute_browser"
] = False # Typically restricted in pure remote unless via API
caps["can_read_files"] = False # Security restriction
self._capabilities = caps
def get_capability(self, key: str) -> bool:
"""Check if a specific capability is active."""
return self._capabilities.get(key, False)
def get_api_url(self) -> str:
"""Get the correct API URL for the current mode."""
if self.config is None:
return "https://dss.overbits.luz.uy" # Default fallback
return self.config.get_api_url(self.active_mode)
def get_strategy(self, strategy_type: str) -> Any:
"""
Factory method to retrieve operational strategies.
Args:
strategy_type: One of 'browser', 'filesystem', 'screenshot'
Returns:
An instance of the requested strategy.
"""
# Return cached strategy if available
if strategy_type in self._strategy_cache:
return self._strategy_cache[strategy_type]
strategy_instance = None
# NOTE: Strategy classes will be implemented in the next step.
# We use local imports here to avoid circular dependency issues
# if strategies define their own types using DSSContext.
try:
if strategy_type == "browser":
# Will be implemented in Phase 2 & 3
if self.active_mode == DSSMode.LOCAL:
from ..strategies.local.browser import LocalBrowserStrategy
strategy_instance = LocalBrowserStrategy(self)
else:
from ..strategies.remote.browser import RemoteBrowserStrategy
strategy_instance = RemoteBrowserStrategy(self)
elif strategy_type == "filesystem":
# Will be implemented in Phase 2
if self.active_mode == DSSMode.LOCAL:
from ..strategies.local.filesystem import LocalFilesystemStrategy
strategy_instance = LocalFilesystemStrategy(self)
else:
from ..strategies.remote.filesystem import RemoteFilesystemStrategy
strategy_instance = RemoteFilesystemStrategy(self)
elif strategy_type == "screenshot":
# Screenshot is part of browser strategy
return self.get_strategy("browser")
else:
raise ValueError(f"Unknown strategy type: {strategy_type}")
except ImportError as e:
logger.error(f"Failed to import strategy {strategy_type}: {e}")
raise NotImplementedError(f"Strategy {strategy_type} not yet implemented") from e
# Cache and return
self._strategy_cache[strategy_type] = strategy_instance
return strategy_instance
# === Local Cache Access Methods ===
def get_local_cache(self) -> Optional[LocalAnalysisCache]:
"""
Get the local analysis cache instance.
Returns:
LocalAnalysisCache instance or None if not in LOCAL mode.
"""
return self._local_cache
def get_cache_validator(self) -> Optional[LocalCacheValidator]:
"""
Get the local cache validator instance.
Returns:
LocalCacheValidator instance or None if not in LOCAL mode.
"""
return self._cache_validator
def get_cache_status(self) -> Dict[str, Any]:
"""
Get current cache status.
Returns:
Cache status dict with freshness info and recommendations.
"""
if self._local_cache is None:
return {
"available": False,
"mode": self.active_mode.value,
"message": f"Local cache not available in {self.active_mode.value} mode"
}
status = self._local_cache.get_cache_status()
status["available"] = True
status["mode"] = self.active_mode.value
return status
def validate_file_local(self, file_path: str) -> Dict[str, Any]:
"""
Validate a file using local cache (LOCAL mode only).
Args:
file_path: Path to file to validate.
Returns:
Validation result dict.
"""
if self._cache_validator is None:
return {
"file": file_path,
"error": "Local cache not available",
"mode": self.active_mode.value
}
return self._cache_validator.validate_file(file_path)
def get_validation_summary(self) -> Dict[str, Any]:
"""
Get summary of validation state from local cache.
Returns:
Summary dict with counts and status.
"""
if self._cache_validator is None:
return {
"error": "Local cache not available",
"mode": self.active_mode.value
}
return self._cache_validator.get_summary()
def get_mode_behavior(self) -> Dict[str, Any]:
"""
Get behavior configuration for current mode.
Returns:
Dict with blocking, upload_metrics, use_cache flags.
"""
if self.config is None:
return {"blocking": False, "upload_metrics": False, "use_cache": False}
return self.config.get_mode_behavior(self.active_mode)