feat(analysis): Implement project analysis engine and CI/CD workflow

This commit introduces a new project analysis engine to the DSS.

Key features include:
- A new analysis module in `dss-mvp1/dss/analyze` that can parse React projects and generate a dependency graph.
- A command-line interface (`dss-mvp1/dss-cli.py`) to run the analysis, designed for use in CI/CD pipelines.
- A new `dss_project_export_context` tool in the Claude MCP server to allow AI agents to access the analysis results.
- A `.gitlab-ci.yml` file to automate the analysis on every push, ensuring the project context is always up-to-date.
- Tests for the new analysis functionality.

This new architecture enables DSS to have a deep, version-controlled understanding of a project's structure, which can be used to power more intelligent agents and provide better developer guidance. The analysis is no longer automatically triggered on `init`, but is designed to be run manually or by a CI/CD pipeline.
This commit is contained in:
Digital Production Factory
2025-12-10 11:05:27 -03:00
parent 842cce133c
commit d53b61008c
15 changed files with 952 additions and 171 deletions

View File

@@ -827,6 +827,20 @@ async def list_tools() -> List[Tool]:
"required": ["project_path"]
}
),
Tool(
name="dss_project_graph_analysis",
description="Generates a dependency graph of the project's components and styles.",
inputSchema={
"type": "object",
"properties": {
"project_path": {
"type": "string",
"description": "Path to the project directory to be analyzed."
}
},
"required": ["project_path"]
}
),
Tool(
name="dss_project_list",
description="List all registered DSS projects.",
@@ -849,6 +863,20 @@ async def list_tools() -> List[Tool]:
"required": ["project_path"]
}
),
Tool(
name="dss_project_export_context",
description="Exports a comprehensive project context, including analysis graph and configuration, for external agents.",
inputSchema={
"type": "object",
"properties": {
"project_path": {
"type": "string",
"description": "Path to the project directory."
}
},
"required": ["project_path"]
}
),
Tool(
name="dss_figma_discover",
description="Discover Figma team structure including all projects, files, and identify UIKit reference file.",
@@ -1169,12 +1197,20 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
result = await project_build_impl(
project_path=arguments.get("project_path")
)
elif name == "dss_project_graph_analysis":
result = await project_graph_analysis_impl(
project_path=arguments.get("project_path")
)
elif name == "dss_project_list":
result = await project_list_impl()
elif name == "dss_project_info":
result = await project_info_impl(
project_path=arguments.get("project_path")
)
elif name == "dss_project_export_context":
result = await project_export_context_impl(
project_path=arguments.get("project_path")
)
elif name == "dss_figma_discover":
result = await figma_discover_impl(
team_id=arguments.get("team_id"),
@@ -2208,108 +2244,148 @@ async def browser_close_impl() -> Dict[str, Any]:
# PROJECT MANAGEMENT IMPLEMENTATIONS
# =============================================================================
async def project_init_impl(
path: str,
name: str,
description: Optional[str] = None,
skin: Optional[str] = None
) -> Dict[str, Any]:
"""Initialize a new DSS project."""
if not PROJECT_MANAGEMENT_AVAILABLE:
return {
"success": False,
"error": f"Project management not available: {PROJECT_MANAGEMENT_IMPORT_ERROR}"
}
async def project_init_impl(path: str, name: str, description: str = None, skin: str = None) -> Dict[str, Any]:
"""Implementation for dss_project_init"""
if not path or not name:
return {"success": False, "error": "path and name are required."}
try:
loop = asyncio.get_event_loop()
manager = ProjectManager()
project = await loop.run_in_executor(
None,
lambda: manager.init(
path=Path(path),
name=name,
description=description,
skin=skin
)
project = manager.init(
path=Path(path),
name=name,
description=description,
skin=skin
)
# Trigger graph analysis in the background
asyncio.create_task(project_graph_analysis_impl(project_path=str(project.path)))
return {
"success": True,
"message": f"Project '{name}' initialized at {path}",
"project": {
"name": project.config.name,
"path": str(project.path),
"status": project.status.value,
"config_file": str(project.config_path)
},
"directories_created": [
"tokens/",
"tokens/figma/",
"tokens/custom/",
"tokens/compiled/",
"themes/",
"components/"
]
"project_name": project.config.name,
"path": str(project.path),
"status": project.status.value,
"message": "Project initialized. Graph analysis started in background."
}
except FileExistsError as e:
return {"success": False, "error": str(e)}
except Exception as e:
logger.exception("dss_project_init failed")
return {"success": False, "error": str(e)}
async def project_add_figma_team_impl(
project_path: str,
team_id: str,
figma_token: Optional[str] = None
) -> Dict[str, Any]:
"""Link a Figma team folder to DSS project."""
if not PROJECT_MANAGEMENT_AVAILABLE:
return {
"success": False,
"error": f"Project management not available: {PROJECT_MANAGEMENT_IMPORT_ERROR}"
}
async def project_graph_analysis_impl(project_path: str) -> Dict[str, Any]:
"""Implementation for dss_project_graph_analysis"""
if not project_path:
return {"success": False, "error": "project_path is required."}
try:
from dss.analyze.project_analyzer import run_project_analysis
loop = asyncio.get_event_loop()
manager = ProjectManager()
# Load existing project
project = await loop.run_in_executor(
None,
lambda: manager.load(Path(project_path))
)
analysis_result = await loop.run_in_executor(None, run_project_analysis, project_path)
# Add Figma team
updated_project = await loop.run_in_executor(
None,
lambda: manager.add_figma_team(
project=project,
team_id=team_id,
figma_token=figma_token
)
)
# Build response
files_info = []
for f in updated_project.config.figma.files:
files_info.append({
"key": f.key,
"name": f.name,
"is_uikit": f.key == updated_project.config.figma.uikit_file_key
})
return {
"success": True,
"message": f"Linked Figma team {team_id} to project",
"team_id": team_id,
"files_discovered": len(files_info),
"files": files_info,
"uikit_file": updated_project.config.figma.uikit_file_key,
"project_status": updated_project.status.value
"project_path": project_path,
"analysis": analysis_result
}
except Exception as e:
logger.exception(f"dss_project_graph_analysis failed for {project_path}")
return {"success": False, "error": str(e)}
async def project_add_figma_team_impl(project_path: str, team_id: str, figma_token: Optional[str] = None) -> Dict[str, Any]:
"""Implementation for dss_project_add_figma_team"""
if not project_path or not team_id:
return {"success": False, "error": "project_path and team_id are required."}
try:
manager = ProjectManager()
project = manager.load(Path(project_path))
updated_project = manager.add_figma_team(
project=project,
team_id=team_id,
figma_token=figma_token
)
return {
"success": True,
"project_name": updated_project.config.name,
"figma_team_id": updated_project.config.figma.team_id,
"files_added": len(updated_project.config.figma.files)
}
except Exception as e:
logger.exception("dss_project_add_figma_team failed")
return {"success": False, "error": str(e)}
@@ -2466,109 +2542,62 @@ async def project_list_impl() -> Dict[str, Any]:
async def project_info_impl(project_path: str) -> Dict[str, Any]:
"""Get detailed project information."""
if not PROJECT_MANAGEMENT_AVAILABLE:
return {
"success": False,
"error": f"Project management not available: {PROJECT_MANAGEMENT_IMPORT_ERROR}"
}
"""Implementation for dss_project_info"""
if not project_path:
return {"success": False, "error": "project_path is required."}
try:
loop = asyncio.get_event_loop()
manager = ProjectManager()
project = await loop.run_in_executor(
None,
lambda: manager.load(Path(project_path))
)
figma_info = None
if project.config.figma:
figma_info = {
"team_id": project.config.figma.team_id,
"project_id": project.config.figma.project_id,
"project_name": project.config.figma.project_name,
"files_count": len(project.config.figma.files),
"uikit_file_key": project.config.figma.uikit_file_key,
"files": [
{"key": f.key, "name": f.name, "last_synced": f.last_synced.isoformat() if f.last_synced else None}
for f in project.config.figma.files
]
}
project = manager.load(Path(project_path))
return {
"success": True,
"project": {
"name": project.config.name,
"version": project.config.version,
"description": project.config.description,
"path": str(project.path),
"status": project.status.value,
"skin": project.config.skin,
"base_theme": project.config.base_theme,
"figma": figma_info,
"output": {
"tokens_dir": project.config.output.tokens_dir,
"themes_dir": project.config.output.themes_dir,
"formats": project.config.output.formats
},
"created_at": project.config.created_at.isoformat(),
"updated_at": project.config.updated_at.isoformat()
}
"project_info": safe_serialize(project.config)
}
except Exception as e:
logger.exception("dss_project_info failed")
return {"success": False, "error": str(e)}
async def figma_discover_impl(
team_id: str,
figma_token: Optional[str] = None
) -> Dict[str, Any]:
"""Discover Figma team structure."""
if not PROJECT_MANAGEMENT_AVAILABLE:
async def project_export_context_impl(project_path: str) -> Dict[str, Any]:
"""Implementation for dss_project_export_context"""
if not project_path:
return {"success": False, "error": "project_path is required."}
try:
from dss.analyze.project_analyzer import export_project_context
loop = asyncio.get_event_loop()
project_context = await loop.run_in_executor(None, export_project_context, project_path)
return {
"success": False,
"error": f"Project management not available: {PROJECT_MANAGEMENT_IMPORT_ERROR}"
"success": True,
"project_context": project_context
}
except Exception as e:
logger.exception(f"dss_project_export_context failed for {project_path}")
return {"success": False, "error": str(e)}
async def project_graph_analysis_impl(project_path: str) -> Dict[str, Any]:
"""Implementation for dss_project_graph_analysis"""
if not project_path:
return {"success": False, "error": "project_path is required."}
try:
from dss.analyze.project_analyzer import run_project_analysis
loop = asyncio.get_event_loop()
sync = FigmaProjectSync(token=figma_token)
structure = await loop.run_in_executor(
None,
lambda: sync.discover_team_structure(team_id)
)
# Format response
projects_info = []
total_files = 0
for proj in structure.get("projects", []):
files = proj.get("files", [])
total_files += len(files)
projects_info.append({
"id": proj["id"],
"name": proj["name"],
"files_count": len(files),
"files": files
})
uikit_info = structure.get("uikit")
analysis_result = await loop.run_in_executor(None, run_project_analysis, project_path)
return {
"success": True,
"team_id": team_id,
"team_name": structure.get("team_name", ""),
"projects_count": len(projects_info),
"total_files": total_files,
"projects": projects_info,
"uikit_reference": uikit_info
"project_path": project_path,
"analysis": analysis_result
}
except ValueError as e:
return {"success": False, "error": str(e)}
except Exception as e:
logger.exception(f"dss_project_graph_analysis failed for {project_path}")
return {"success": False, "error": str(e)}
async def figma_discover_impl(team_id: str, figma_token: Optional[str] = None) -> Dict[str, Any]:
"""Implementation for dss_figma_discover"""
# =============================================================================
# DSS CORE SYNC IMPLEMENTATIONS