refactor: rename action→activity, statemachine→workflow, remove HTTP API layer
- action/ → activity/ (Temporal activities) - statemachine/ → workflow/ (Temporal workflows) - Removed internal/api/ and cmd/server/ (api-gw handles HTTP, Temporal is the API) - Created pkg/types/types.go as single source of truth for all shared types - Extracted CallRoleLLM helper (DRY: implementer/planner/judge shared pattern) - Fixed circular import: workflow_graph_query uses string activity names - Fixed logger.logf → logger.Info/Warn (method didn't exist) - Fixed routing types: added Branches, Activity, BackoffSeconds, TaskActivity - Fixed db.Canvas.Name, db.Client→DB, GetWorkflow→FetchWorkflow - Removed unused imports - All tests pass, build clean, vet clean
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
package activity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AnalyzeCodeInput is input for AnalyzeCodeActivity
|
||||
type AnalyzeCodeInput struct {
|
||||
Path string `json:"path"`
|
||||
Language string `json:"language,omitempty"`
|
||||
Depth int `json:"depth,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyzeCodeOutput is output from AnalyzeCodeActivity
|
||||
type AnalyzeCodeOutput struct {
|
||||
Quality float64 `json:"quality"`
|
||||
Metrics map[string]interface{} `json:"metrics"`
|
||||
Issues []string `json:"issues"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// AnalyzeCodeActivity analyzes code quality using available tools
|
||||
func AnalyzeCodeActivity(ctx context.Context, in AnalyzeCodeInput) (AnalyzeCodeOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("AnalyzeCodeActivity started", "path", in.Path)
|
||||
|
||||
output := AnalyzeCodeOutput{
|
||||
Quality: 0.0,
|
||||
Metrics: make(map[string]interface{}),
|
||||
Issues: []string{},
|
||||
}
|
||||
|
||||
// Verify path exists
|
||||
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
|
||||
return output, fmt.Errorf("path does not exist: %s", in.Path)
|
||||
}
|
||||
|
||||
// Count files and lines
|
||||
var totalFiles, totalLines int
|
||||
err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // Skip errors
|
||||
}
|
||||
if info.IsDir() {
|
||||
// Skip hidden and vendor directories
|
||||
if strings.HasPrefix(info.Name(), ".") || info.Name() == "vendor" || info.Name() == "node_modules" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
if isCodeFile(ext) {
|
||||
totalFiles++
|
||||
if lines, err := countLines(path); err == nil {
|
||||
totalLines += lines
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Error walking path", "error", err)
|
||||
}
|
||||
|
||||
output.Metrics["totalFiles"] = totalFiles
|
||||
output.Metrics["totalLines"] = totalLines
|
||||
|
||||
// Try to run go vet if it's a Go project
|
||||
if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil {
|
||||
cmd := exec.CommandContext(ctx, "go", "vet", "./...")
|
||||
cmd.Dir = in.Path
|
||||
vetOutput, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
issues := strings.Split(string(vetOutput), "\n")
|
||||
for _, issue := range issues {
|
||||
if strings.TrimSpace(issue) != "" {
|
||||
output.Issues = append(output.Issues, issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
output.Metrics["goVetRan"] = true
|
||||
}
|
||||
|
||||
// Calculate quality score (simple heuristic)
|
||||
issueCount := len(output.Issues)
|
||||
if totalFiles > 0 {
|
||||
issuesPerFile := float64(issueCount) / float64(totalFiles)
|
||||
output.Quality = max(0, 1.0 - (issuesPerFile * 0.1))
|
||||
} else {
|
||||
output.Quality = 0.5
|
||||
}
|
||||
|
||||
output.Summary = fmt.Sprintf("Analyzed %d files (%d lines). Found %d issues. Quality score: %.2f",
|
||||
totalFiles, totalLines, issueCount, output.Quality)
|
||||
|
||||
logger.Info("AnalyzeCodeActivity completed", "quality", output.Quality, "issues", issueCount)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// SecurityScanInput is input for SecurityScanActivity
|
||||
type SecurityScanInput struct {
|
||||
Path string `json:"path"`
|
||||
Severity string `json:"severity,omitempty"` // low, medium, high, critical
|
||||
}
|
||||
|
||||
// SecurityScanOutput is output from SecurityScanActivity
|
||||
type SecurityScanOutput struct {
|
||||
Vulnerabilities []Vulnerability `json:"vulnerabilities"`
|
||||
SecurityScore float64 `json:"securityScore"`
|
||||
RiskLevel string `json:"riskLevel"`
|
||||
}
|
||||
|
||||
// Vulnerability represents a security issue
|
||||
type Vulnerability struct {
|
||||
ID string `json:"id"`
|
||||
Severity string `json:"severity"`
|
||||
Description string `json:"description"`
|
||||
File string `json:"file,omitempty"`
|
||||
Line int `json:"line,omitempty"`
|
||||
}
|
||||
|
||||
// SecurityScanActivity scans code for security vulnerabilities
|
||||
func SecurityScanActivity(ctx context.Context, in SecurityScanInput) (SecurityScanOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("SecurityScanActivity started", "path", in.Path)
|
||||
|
||||
output := SecurityScanOutput{
|
||||
Vulnerabilities: []Vulnerability{},
|
||||
SecurityScore: 100.0,
|
||||
RiskLevel: "low",
|
||||
}
|
||||
|
||||
// Verify path exists
|
||||
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
|
||||
return output, fmt.Errorf("path does not exist: %s", in.Path)
|
||||
}
|
||||
|
||||
// Check for common security issues
|
||||
// 1. Check for hardcoded secrets
|
||||
secretPatterns := []string{
|
||||
"password=",
|
||||
"secret=",
|
||||
"api_key=",
|
||||
"apikey=",
|
||||
"private_key",
|
||||
"AWS_SECRET",
|
||||
}
|
||||
|
||||
err := filepath.Walk(in.Path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !isCodeFile(filepath.Ext(path)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentStr := strings.ToLower(string(content))
|
||||
for _, pattern := range secretPatterns {
|
||||
if strings.Contains(contentStr, pattern) {
|
||||
output.Vulnerabilities = append(output.Vulnerabilities, Vulnerability{
|
||||
ID: fmt.Sprintf("SEC-%d", len(output.Vulnerabilities)+1),
|
||||
Severity: "high",
|
||||
Description: fmt.Sprintf("Possible hardcoded secret: %s", pattern),
|
||||
File: path,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Error scanning", "error", err)
|
||||
}
|
||||
|
||||
// Try gosec if available and it's a Go project
|
||||
if _, err := os.Stat(filepath.Join(in.Path, "go.mod")); err == nil {
|
||||
if _, err := exec.LookPath("gosec"); err == nil {
|
||||
cmd := exec.CommandContext(ctx, "gosec", "-fmt=json", "-quiet", "./...")
|
||||
cmd.Dir = in.Path
|
||||
// gosec returns non-zero if issues found, so we ignore the error
|
||||
cmd.CombinedOutput()
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate score
|
||||
vulnCount := len(output.Vulnerabilities)
|
||||
if vulnCount == 0 {
|
||||
output.SecurityScore = 100.0
|
||||
output.RiskLevel = "low"
|
||||
} else if vulnCount < 3 {
|
||||
output.SecurityScore = 80.0
|
||||
output.RiskLevel = "medium"
|
||||
} else if vulnCount < 10 {
|
||||
output.SecurityScore = 50.0
|
||||
output.RiskLevel = "high"
|
||||
} else {
|
||||
output.SecurityScore = 20.0
|
||||
output.RiskLevel = "critical"
|
||||
}
|
||||
|
||||
logger.Info("SecurityScanActivity completed", "vulnerabilities", vulnCount, "riskLevel", output.RiskLevel)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// GenerateReportInput is input for GenerateReportActivity
|
||||
type GenerateReportInput struct {
|
||||
AnalysisResult interface{} `json:"analysisResult"`
|
||||
SecurityResult interface{} `json:"securityResult"`
|
||||
Format string `json:"format,omitempty"` // markdown, html, json
|
||||
}
|
||||
|
||||
// GenerateReportOutput is output from GenerateReportActivity
|
||||
type GenerateReportOutput struct {
|
||||
Report string `json:"report"`
|
||||
ReportPath string `json:"reportPath"`
|
||||
}
|
||||
|
||||
// GenerateReportActivity generates a combined report
|
||||
func GenerateReportActivity(ctx context.Context, in GenerateReportInput) (GenerateReportOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("GenerateReportActivity started")
|
||||
|
||||
format := in.Format
|
||||
if format == "" {
|
||||
format = "markdown"
|
||||
}
|
||||
|
||||
var report strings.Builder
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
switch format {
|
||||
case "markdown":
|
||||
report.WriteString("# Analysis Report\n\n")
|
||||
report.WriteString(fmt.Sprintf("Generated: %s\n\n", timestamp))
|
||||
report.WriteString("## Code Analysis\n\n")
|
||||
report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.AnalysisResult))
|
||||
report.WriteString("## Security Scan\n\n")
|
||||
report.WriteString(fmt.Sprintf("```\n%v\n```\n\n", in.SecurityResult))
|
||||
case "json":
|
||||
report.WriteString(fmt.Sprintf(`{"timestamp":"%s","analysis":%v,"security":%v}`,
|
||||
timestamp, in.AnalysisResult, in.SecurityResult))
|
||||
default:
|
||||
report.WriteString(fmt.Sprintf("Report generated at %s\n", timestamp))
|
||||
report.WriteString(fmt.Sprintf("Analysis: %v\n", in.AnalysisResult))
|
||||
report.WriteString(fmt.Sprintf("Security: %v\n", in.SecurityResult))
|
||||
}
|
||||
|
||||
// Save to temp file
|
||||
reportPath := filepath.Join(os.TempDir(), fmt.Sprintf("report-%d.%s", time.Now().UnixNano(), format))
|
||||
if err := os.WriteFile(reportPath, []byte(report.String()), 0644); err != nil {
|
||||
logger.Warn("Failed to save report", "error", err)
|
||||
}
|
||||
|
||||
logger.Info("GenerateReportActivity completed", "format", format)
|
||||
return GenerateReportOutput{
|
||||
Report: report.String(),
|
||||
ReportPath: reportPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func isCodeFile(ext string) bool {
|
||||
codeExts := map[string]bool{
|
||||
".go": true, ".py": true, ".js": true, ".ts": true,
|
||||
".java": true, ".c": true, ".cpp": true, ".h": true,
|
||||
".rs": true, ".rb": true, ".php": true, ".swift": true,
|
||||
}
|
||||
return codeExts[ext]
|
||||
}
|
||||
|
||||
func countLines(path string) (int, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(strings.Split(string(content), "\n")), nil
|
||||
}
|
||||
|
||||
func max(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user