Major refactor: Consolidate DSS into unified package structure

- Create new dss/ Python package at project root
- Move MCP core from tools/dss_mcp/ to dss/mcp/
- Move storage layer from tools/storage/ to dss/storage/
- Move domain logic from dss-mvp1/dss/ to dss/
- Move services from tools/api/services/ to dss/services/
- Move API server to apps/api/
- Move CLI to apps/cli/
- Move Storybook assets to storybook/
- Create unified dss/__init__.py with comprehensive exports
- Merge configuration into dss/settings.py (Pydantic-based)
- Create pyproject.toml for proper package management
- Update startup scripts for new paths
- Remove old tools/ and dss-mvp1/ directories

Architecture changes:
- DSS is now MCP-first with 40+ tools for Claude Code
- Clean imports: from dss import Projects, Components, FigmaToolSuite
- No more sys.path.insert() hacking
- apps/ contains thin application wrappers (API, CLI)
- Single unified Python package for all DSS logic

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-10 12:43:18 -03:00
parent bbd67f88c4
commit 41fba59bf7
197 changed files with 3185 additions and 15500 deletions

View File

@@ -1,98 +1,82 @@
"""
Pytest configuration and shared fixtures.
"""
import pytest
import tempfile
import shutil
from pathlib import Path
from tools.ingest.base import DesignToken, TokenCollection, TokenType
@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
temp_path = tempfile.mkdtemp()
yield Path(temp_path)
shutil.rmtree(temp_path, ignore_errors=True)
@pytest.fixture
def sample_css():
"""Sample CSS custom properties."""
return """
:root {
--color-primary: #3B82F6;
--color-secondary: #10B981;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--font-size-base: 16px;
}
@pytest.fixture(scope="function")
def mock_react_project(tmp_path: Path) -> Path:
"""
@pytest.fixture
def sample_scss():
"""Sample SCSS variables."""
return """
$primary-color: #3B82F6;
$secondary-color: #10B981;
$font-family-sans: 'Inter', sans-serif;
$font-size-base: 16px;
$spacing-md: 16px;
Creates a temporary mock React project structure for testing.
"""
project_dir = tmp_path / "test-project"
project_dir.mkdir()
# Create src directory
src_dir = project_dir / "src"
src_dir.mkdir()
@pytest.fixture
def sample_json_tokens():
"""Sample JSON design tokens (W3C format)."""
return {
"color": {
"primary": {
"500": {"value": "#3B82F6", "type": "color"},
"600": {"value": "#2563EB", "type": "color"}
},
"secondary": {
"500": {"value": "#10B981", "type": "color"}
}
},
"spacing": {
"sm": {"value": "8px", "type": "dimension"},
"md": {"value": "16px", "type": "dimension"},
"lg": {"value": "24px", "type": "dimension"}
}
}
# Create components directory
components_dir = src_dir / "components"
components_dir.mkdir()
# Component A
(components_dir / "ComponentA.jsx").write_text("""
import React from 'react';
import './ComponentA.css';
@pytest.fixture
def sample_token_collection():
"""Create a sample token collection."""
tokens = [
DesignToken(name="color.primary", value="#3B82F6", type=TokenType.COLOR),
DesignToken(name="color.secondary", value="#10B981", type=TokenType.COLOR),
DesignToken(name="spacing.md", value="16px", type=TokenType.SPACING),
]
return TokenCollection(tokens=tokens, name="Sample Collection")
const ComponentA = () => {
return <div className="component-a">Component A</div>;
};
export default ComponentA;
""")
@pytest.fixture
def tailwind_config_path(temp_dir):
"""Create a temporary Tailwind config file."""
config_content = """
module.exports = {
theme: {
colors: {
blue: '#0000FF',
red: '#FF0000'
},
spacing: {
'1': '4px',
'2': '8px'
}
}
(components_dir / "ComponentA.css").write_text("""
.component-a {
color: blue;
}
"""
config_file = temp_dir / "tailwind.config.js"
config_file.write_text(config_content)
return config_file
""")
# Component B
(components_dir / "ComponentB.tsx").write_text("""
import React from 'react';
import ComponentA from './ComponentA';
const ComponentB = () => {
return (
<div>
<ComponentA />
</div>
);
};
export default ComponentB;
""")
# App.js
(src_dir / "App.js").write_text("""
import React from 'react';
import ComponentB from './components/ComponentB';
function App() {
return (
<div className="App">
<ComponentB />
</div>
);
}
export default App;
""")
# package.json
(project_dir / "package.json").write_text("""
{
"name": "test-project",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^18.0.0"
}
}
""")
return project_dir

View File

@@ -1,68 +0,0 @@
"""
Tests for token ingestion from various sources.
"""
import pytest
import json
from tools.ingest.css import CSSTokenSource
from tools.ingest.scss import SCSSTokenSource
from tools.ingest.json_tokens import JSONTokenSource
@pytest.mark.asyncio
async def test_css_ingestion(sample_css):
"""Test CSS custom property extraction."""
parser = CSSTokenSource()
result = await parser.extract(sample_css)
assert len(result.tokens) >= 5
assert any(t.name == "color.primary" for t in result.tokens)
assert any(t.value == "#3B82F6" for t in result.tokens)
@pytest.mark.asyncio
async def test_scss_ingestion(sample_scss):
"""Test SCSS variable extraction."""
parser = SCSSTokenSource()
result = await parser.extract(sample_scss)
assert len(result.tokens) >= 4
# SCSS converts $primary-color to primary.color (dashes to dots)
assert any(t.name == "primary.color" for t in result.tokens)
@pytest.mark.asyncio
async def test_json_ingestion(sample_json_tokens):
"""Test JSON token extraction (W3C format)."""
parser = JSONTokenSource()
result = await parser.extract(json.dumps(sample_json_tokens))
assert len(result.tokens) >= 6
# Check color tokens
primary_tokens = [t for t in result.tokens if "primary" in t.name]
assert len(primary_tokens) >= 2
# Check spacing tokens
spacing_tokens = [t for t in result.tokens if "spacing" in t.name]
assert len(spacing_tokens) == 3
@pytest.mark.asyncio
async def test_empty_css():
"""Test handling of empty CSS."""
parser = CSSTokenSource()
# Use CSS syntax marker so parser detects as content, not file path
result = await parser.extract(":root {}")
assert len(result.tokens) == 0
assert result.name
@pytest.mark.asyncio
async def test_invalid_json():
"""Test handling of invalid JSON."""
parser = JSONTokenSource()
# Parser wraps JSONDecodeError in ValueError
with pytest.raises(ValueError, match="Invalid JSON"):
await parser.extract("invalid json{")

View File

@@ -1,308 +0,0 @@
"""
Tests for JSON file storage layer.
Tests the new json_store module that replaced SQLite.
"""
import pytest
import json
import tempfile
import shutil
from pathlib import Path
from datetime import datetime
# Temporarily override DATA_DIR for tests
import tools.storage.json_store as json_store
@pytest.fixture
def temp_storage(tmp_path):
"""Create temporary storage directory for tests."""
# Save original paths
original_data_dir = json_store.DATA_DIR
original_system_dir = json_store.SYSTEM_DIR
original_projects_dir = json_store.PROJECTS_DIR
original_teams_dir = json_store.TEAMS_DIR
# Override with temp paths
json_store.DATA_DIR = tmp_path / "data"
json_store.SYSTEM_DIR = json_store.DATA_DIR / "_system"
json_store.PROJECTS_DIR = json_store.DATA_DIR / "projects"
json_store.TEAMS_DIR = json_store.DATA_DIR / "teams"
json_store.Cache.CACHE_DIR = json_store.SYSTEM_DIR / "cache"
# Initialize directories
json_store.init_storage()
yield tmp_path
# Restore original paths
json_store.DATA_DIR = original_data_dir
json_store.SYSTEM_DIR = original_system_dir
json_store.PROJECTS_DIR = original_projects_dir
json_store.TEAMS_DIR = original_teams_dir
json_store.Cache.CACHE_DIR = original_system_dir / "cache"
class TestCache:
"""Tests for TTL-based cache."""
def test_cache_set_and_get(self, temp_storage):
"""Test basic cache operations."""
json_store.Cache.set("test_key", {"foo": "bar"}, ttl=60)
result = json_store.Cache.get("test_key")
assert result == {"foo": "bar"}
def test_cache_expiry(self, temp_storage):
"""Test that expired cache returns None."""
import time
json_store.Cache.set("expired_key", "value", ttl=1)
time.sleep(1.1) # Wait for expiry
result = json_store.Cache.get("expired_key")
assert result is None
def test_cache_delete(self, temp_storage):
"""Test cache deletion."""
json_store.Cache.set("delete_me", "value")
json_store.Cache.delete("delete_me")
result = json_store.Cache.get("delete_me")
assert result is None
def test_cache_clear_all(self, temp_storage):
"""Test clearing all cache."""
json_store.Cache.set("key1", "value1")
json_store.Cache.set("key2", "value2")
json_store.Cache.clear_all()
assert json_store.Cache.get("key1") is None
assert json_store.Cache.get("key2") is None
class TestProjects:
"""Tests for project operations."""
def test_create_project(self, temp_storage):
"""Test project creation."""
project = json_store.Projects.create(
id="test-project",
name="Test Project",
description="A test project"
)
assert project["id"] == "test-project"
assert project["name"] == "Test Project"
assert project["status"] == "active"
def test_get_project(self, temp_storage):
"""Test project retrieval."""
json_store.Projects.create(id="get-test", name="Get Test")
project = json_store.Projects.get("get-test")
assert project is not None
assert project["name"] == "Get Test"
def test_list_projects(self, temp_storage):
"""Test listing projects."""
json_store.Projects.create(id="proj1", name="Project 1")
json_store.Projects.create(id="proj2", name="Project 2")
projects = json_store.Projects.list()
assert len(projects) == 2
def test_update_project(self, temp_storage):
"""Test project update."""
json_store.Projects.create(id="update-test", name="Original")
updated = json_store.Projects.update("update-test", name="Updated")
assert updated["name"] == "Updated"
def test_delete_project(self, temp_storage):
"""Test project deletion (archives)."""
json_store.Projects.create(id="delete-test", name="Delete Me")
result = json_store.Projects.delete("delete-test")
assert result is True
assert json_store.Projects.get("delete-test") is None
def test_project_creates_token_structure(self, temp_storage):
"""Test that project creation initializes token folders."""
json_store.Projects.create(id="token-test", name="Token Test")
tokens_dir = json_store.PROJECTS_DIR / "token-test" / "tokens"
assert tokens_dir.exists()
assert (tokens_dir / "colors.json").exists()
assert (tokens_dir / "spacing.json").exists()
class TestTokens:
"""Tests for token operations."""
def test_get_all_tokens(self, temp_storage):
"""Test getting all tokens for a project."""
json_store.Projects.create(id="tokens-proj", name="Tokens Project")
tokens = json_store.Tokens.get_all("tokens-proj")
assert "colors" in tokens
assert "spacing" in tokens
assert "typography" in tokens
def test_set_and_get_tokens(self, temp_storage):
"""Test setting and getting tokens by type."""
json_store.Projects.create(id="set-tokens", name="Set Tokens")
json_store.Tokens.set_by_type("set-tokens", "colors", {
"primary": "#3B82F6",
"secondary": "#10B981"
})
colors = json_store.Tokens.get_by_type("set-tokens", "colors")
assert colors["primary"] == "#3B82F6"
assert colors["secondary"] == "#10B981"
def test_merge_tokens_last_strategy(self, temp_storage):
"""Test merging tokens with LAST strategy."""
json_store.Projects.create(id="merge-test", name="Merge Test")
json_store.Tokens.set_by_type("merge-test", "colors", {
"primary": "#old",
"secondary": "#keep"
})
merged = json_store.Tokens.merge("merge-test", "colors", {
"primary": "#new",
"tertiary": "#added"
}, strategy="LAST")
assert merged["primary"] == "#new"
assert merged["secondary"] == "#keep"
assert merged["tertiary"] == "#added"
class TestComponents:
"""Tests for component operations."""
def test_upsert_components(self, temp_storage):
"""Test bulk component upsert."""
json_store.Projects.create(id="comp-proj", name="Component Project")
count = json_store.Components.upsert("comp-proj", [
{"name": "Button", "properties": {"variant": "primary"}},
{"name": "Card", "properties": {"shadow": "md"}}
])
assert count == 2
def test_list_components(self, temp_storage):
"""Test listing components."""
json_store.Projects.create(id="list-comp", name="List Components")
json_store.Components.upsert("list-comp", [
{"name": "Button"},
{"name": "Input"}
])
components = json_store.Components.list("list-comp")
assert len(components) == 2
names = [c["name"] for c in components]
assert "Button" in names
assert "Input" in names
class TestActivityLog:
"""Tests for activity logging."""
def test_log_activity(self, temp_storage):
"""Test logging an activity."""
json_store.ActivityLog.log(
action="test_action",
entity_type="test",
entity_name="Test Entity",
project_id="test-proj"
)
recent = json_store.ActivityLog.recent(limit=1)
assert len(recent) == 1
assert recent[0]["action"] == "test_action"
def test_activity_auto_category(self, temp_storage):
"""Test that activity auto-detects category."""
json_store.ActivityLog.log(action="extract_tokens")
recent = json_store.ActivityLog.recent(limit=1)
assert recent[0]["category"] == "design_system"
class TestTeams:
"""Tests for team operations."""
def test_create_team(self, temp_storage):
"""Test team creation."""
team = json_store.Teams.create(
id="test-team",
name="Test Team",
description="A test team"
)
assert team["id"] == "test-team"
assert team["name"] == "Test Team"
def test_add_member(self, temp_storage):
"""Test adding team member."""
json_store.Teams.create(id="member-team", name="Member Team")
json_store.Teams.add_member("member-team", "user-123", "DEVELOPER")
members = json_store.Teams.get_members("member-team")
assert len(members) == 1
assert members[0]["user_id"] == "user-123"
assert members[0]["role"] == "DEVELOPER"
def test_get_user_role(self, temp_storage):
"""Test getting user role in team."""
json_store.Teams.create(id="role-team", name="Role Team")
json_store.Teams.add_member("role-team", "admin-user", "SUPER_ADMIN")
role = json_store.Teams.get_user_role("role-team", "admin-user")
assert role == "SUPER_ADMIN"
class TestSyncHistory:
"""Tests for sync history."""
def test_sync_lifecycle(self, temp_storage):
"""Test sync start and complete."""
json_store.Projects.create(id="sync-proj", name="Sync Project")
sync_id = json_store.SyncHistory.start("sync-proj", "tokens")
json_store.SyncHistory.complete("sync-proj", sync_id, "success", items_synced=10)
recent = json_store.SyncHistory.recent("sync-proj", limit=5)
# Should have both start and complete records
completed = [r for r in recent if r.get("status") == "success"]
assert len(completed) >= 1
class TestStats:
"""Tests for storage statistics."""
def test_get_stats(self, temp_storage):
"""Test getting storage stats."""
json_store.Projects.create(id="stats-proj", name="Stats Project")
json_store.Teams.create(id="stats-team", name="Stats Team")
stats = json_store.get_stats()
# Stats count directories, verify basic structure
assert "projects" in stats
assert "teams" in stats
assert "total_size_mb" in stats
assert stats["total_size_mb"] >= 0

View File

@@ -1,109 +0,0 @@
"""
Tests for token merging and conflict resolution.
"""
import pytest
from tools.ingest.merge import TokenMerger, MergeStrategy
from tools.ingest.base import TokenCollection, DesignToken, TokenType
def test_merge_no_conflicts():
"""Test merging collections with no conflicts."""
col1 = TokenCollection([
DesignToken(name="color.red", value="#FF0000", type=TokenType.COLOR)
])
col2 = TokenCollection([
DesignToken(name="color.blue", value="#0000FF", type=TokenType.COLOR)
])
merger = TokenMerger(strategy=MergeStrategy.LAST)
result = merger.merge([col1, col2])
assert len(result.collection.tokens) == 2
assert len(result.conflicts) == 0
def test_merge_strategy_first():
"""Test FIRST merge strategy."""
col1 = TokenCollection([
DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css")
])
col2 = TokenCollection([
DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="figma")
])
merger = TokenMerger(strategy=MergeStrategy.FIRST)
result = merger.merge([col1, col2])
assert len(result.collection.tokens) == 1
assert len(result.conflicts) == 1
# Should keep first value
token = result.collection.tokens[0]
assert token.value == "#FF0000"
assert token.source == "css"
def test_merge_strategy_last():
"""Test LAST merge strategy."""
col1 = TokenCollection([
DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css")
])
col2 = TokenCollection([
DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="figma")
])
merger = TokenMerger(strategy=MergeStrategy.LAST)
result = merger.merge([col1, col2])
assert len(result.collection.tokens) == 1
assert len(result.conflicts) == 1
# Should keep last value
token = result.collection.tokens[0]
assert token.value == "#0000FF"
assert token.source == "figma"
def test_merge_strategy_prefer_figma():
"""Test PREFER_FIGMA merge strategy."""
col1 = TokenCollection([
DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css")
])
col2 = TokenCollection([
DesignToken(name="color.primary", value="#3B82F6", type=TokenType.COLOR, source="figma")
])
col3 = TokenCollection([
DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="scss")
])
merger = TokenMerger(strategy=MergeStrategy.PREFER_FIGMA)
result = merger.merge([col1, col2, col3])
# Should prefer Figma value
token = result.collection.tokens[0]
assert token.value == "#3B82F6"
assert token.source == "figma"
def test_merge_multiple_collections():
"""Test merging many collections."""
collections = [
TokenCollection([
DesignToken(name=f"color.{i}", value=f"#{i:06X}", type=TokenType.COLOR)
])
for i in range(10)
]
merger = TokenMerger(strategy=MergeStrategy.LAST)
result = merger.merge(collections)
assert len(result.collection.tokens) == 10
assert len(result.conflicts) == 0
def test_merge_empty_collections():
"""Test merging empty collections."""
merger = TokenMerger(strategy=MergeStrategy.LAST)
result = merger.merge([])
assert len(result.collection.tokens) == 0
assert len(result.conflicts) == 0

View File

@@ -0,0 +1,45 @@
import pytest
import json
from pathlib import Path
from dss.analyze.project_analyzer import run_project_analysis
def test_run_project_analysis(mock_react_project: Path):
"""
Tests the run_project_analysis function to ensure it creates the analysis graph
and that the graph contains the expected file nodes.
"""
# Run the analysis on the mock project
run_project_analysis(str(mock_react_project))
# Check if the analysis file was created
analysis_file = mock_react_project / ".dss" / "analysis_graph.json"
assert analysis_file.exists(), "The analysis_graph.json file was not created."
# Load the analysis data
with open(analysis_file, 'r') as f:
data = json.load(f)
# Verify the graph structure
assert "nodes" in data, "Graph data should contain 'nodes'."
assert "links" in data, "Graph data should contain 'links'."
# Get a list of node IDs (which are the relative file paths)
node_ids = [node['id'] for node in data['nodes']]
# Check for the presence of the files from the mock project
expected_files = [
"package.json",
"src/App.js",
"src/components/ComponentA.css",
"src/components/ComponentA.jsx",
"src/components/ComponentB.tsx",
]
for file_path in expected_files:
# Path separators might be different on different OSes, so we normalize
normalized_path = str(Path(file_path))
assert normalized_path in node_ids, f"Expected file '{normalized_path}' not found in the analysis graph."
# Verify the number of nodes
# There should be exactly the number of files we created
assert len(node_ids) == len(expected_files), "The number of nodes in the graph does not match the number of files."