Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
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>
313 lines
10 KiB
Python
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)
|