Files
dss/dss-mvp1/dss/translations/resolver.py
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

254 lines
8.7 KiB
Python

"""
Token Resolver
Resolves tokens between source formats and DSS canonical structure.
Supports bidirectional translation.
"""
from typing import Any, Dict, List, Optional, Union
from .canonical import DSS_CANONICAL_TOKENS
from .models import ResolvedToken, TranslationRegistry, TranslationSource
class TokenResolver:
"""
Resolves tokens between source and DSS canonical formats.
Supports:
- Source -> DSS translation (forward)
- DSS -> Source translation (reverse)
- Token path resolution with aliasing
- Reference chain resolution
Usage:
resolver = TokenResolver(registry)
# Forward translation
dss_token = resolver.resolve_to_dss("--brand-blue")
# -> "color.primary.500"
# Reverse translation
source_token = resolver.resolve_to_source("color.primary.500", "css")
# -> "--brand-blue"
"""
def __init__(self, registry: TranslationRegistry):
"""
Initialize resolver with translation registry.
Args:
registry: Loaded TranslationRegistry with mappings
"""
self.registry = registry
self._reverse_map: Dict[str, Dict[str, str]] = {}
self._build_reverse_maps()
def _build_reverse_maps(self) -> None:
"""Build reverse lookup maps (DSS -> source) for each source type."""
for source_type, dictionary in self.registry.dictionaries.items():
self._reverse_map[source_type] = {
dss: source for source, dss in dictionary.mappings.tokens.items()
}
def resolve_to_dss(
self, source_token: str, source_type: Optional[Union[str, TranslationSource]] = None
) -> Optional[str]:
"""
Resolve source token to DSS canonical path.
Args:
source_token: Source token (e.g., "--brand-blue", "$primary")
source_type: Optional source type hint (searches all if not provided)
Returns:
DSS canonical path or None if not found
"""
# Direct lookup in combined map
if source_token in self.registry.combined_token_map:
return self.registry.combined_token_map[source_token]
# If source type specified, look only there
if source_type:
if isinstance(source_type, str):
source_type = TranslationSource(source_type)
dictionary = self.registry.dictionaries.get(source_type.value)
if dictionary:
return dictionary.mappings.tokens.get(source_token)
# Try normalization patterns
normalized = self._normalize_token_name(source_token)
return self.registry.combined_token_map.get(normalized)
def resolve_to_source(self, dss_token: str, source_type: Union[str, TranslationSource]) -> Optional[str]:
"""
Resolve DSS token to source format (reverse translation).
Args:
dss_token: DSS canonical path (e.g., "color.primary.500")
source_type: Target source type
Returns:
Source token name or None if not mapped
"""
if isinstance(source_type, str):
source_type_str = source_type
else:
source_type_str = source_type.value
reverse_map = self._reverse_map.get(source_type_str, {})
return reverse_map.get(dss_token)
def resolve_token_value(
self,
source_token: str,
base_theme_tokens: Dict[str, Any],
source_type: Optional[Union[str, TranslationSource]] = None,
) -> Optional[ResolvedToken]:
"""
Fully resolve a source token to its DSS value.
Args:
source_token: Source token name
base_theme_tokens: Base theme token values
source_type: Optional source type hint
Returns:
ResolvedToken with full provenance or None
"""
# Get DSS path
dss_path = self.resolve_to_dss(source_token, source_type)
if not dss_path:
# Check if it's a custom prop
if source_token in self.registry.all_custom_props:
return ResolvedToken(
dss_path=source_token,
value=self.registry.all_custom_props[source_token],
source_token=source_token,
is_custom=True,
provenance=[f"custom_prop: {source_token}"],
)
return None
# Resolve value from base theme
value = self._get_token_value(dss_path, base_theme_tokens)
# Determine source type if not provided
resolved_source = source_type
if resolved_source is None:
for src_type, dictionary in self.registry.dictionaries.items():
if source_token in dictionary.mappings.tokens:
resolved_source = TranslationSource(src_type)
break
return ResolvedToken(
dss_path=dss_path,
value=value,
source_token=source_token,
source_type=resolved_source
if isinstance(resolved_source, TranslationSource)
else (TranslationSource(resolved_source) if resolved_source else None),
is_custom=False,
provenance=[
f"source: {source_token}",
f"mapped_to: {dss_path}",
f"value: {value}",
],
)
def resolve_all_mappings(self, base_theme_tokens: Dict[str, Any]) -> Dict[str, ResolvedToken]:
"""
Resolve all mapped tokens to their DSS values.
Args:
base_theme_tokens: Base theme token values
Returns:
Dict of DSS path -> ResolvedToken
"""
resolved = {}
# Resolve all mapped tokens
for source_token, dss_path in self.registry.combined_token_map.items():
value = self._get_token_value(dss_path, base_theme_tokens)
# Find source type
source_type = None
for src_type, dictionary in self.registry.dictionaries.items():
if source_token in dictionary.mappings.tokens:
source_type = TranslationSource(src_type)
break
resolved[dss_path] = ResolvedToken(
dss_path=dss_path,
value=value,
source_token=source_token,
source_type=source_type,
is_custom=False,
provenance=[f"source: {source_token}", f"mapped_to: {dss_path}"],
)
# Add custom props
for prop_name, prop_value in self.registry.all_custom_props.items():
resolved[prop_name] = ResolvedToken(
dss_path=prop_name,
value=prop_value,
is_custom=True,
provenance=[f"custom_prop: {prop_name}"],
)
return resolved
def _get_token_value(self, dss_path: str, base_tokens: Dict[str, Any]) -> Any:
"""Get token value from base theme using DSS path."""
# Handle nested paths (e.g., "color.primary.500")
parts = dss_path.split(".")
current = base_tokens
for part in parts:
if isinstance(current, dict):
current = current.get(part)
if current is None:
break
else:
return None
# If we got a DesignToken object, extract value
if hasattr(current, "value"):
return current.value
return current
def _normalize_token_name(self, token: str) -> str:
"""Normalize token name for lookup."""
# Remove common prefixes
normalized = token.lstrip("-$")
# Convert various formats to dot notation
normalized = normalized.replace("-", ".").replace("_", ".")
# Handle var() references
if normalized.startswith("var(") and normalized.endswith(")"):
normalized = normalized[4:-1].lstrip("-")
return normalized.lower()
def get_unmapped_tokens(self) -> List[str]:
"""Get list of tokens that couldn't be mapped."""
unmapped = []
for dictionary in self.registry.dictionaries.values():
unmapped.extend(dictionary.unmapped)
return list(set(unmapped))
def validate_dss_path(self, path: str) -> bool:
"""Validate that a path matches DSS canonical structure."""
return path in DSS_CANONICAL_TOKENS or self._is_valid_custom_namespace(path)
def _is_valid_custom_namespace(self, path: str) -> bool:
"""Check if path uses valid custom namespace."""
parts = path.split(".")
if len(parts) < 3:
return False
# Custom props should be like: color.brand.acme.primary
return parts[1] in ("brand", "custom")