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
254 lines
8.7 KiB
Python
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")
|