Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ permissions:
actions: write

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# lint:
# name: Lint
# runs-on: ubuntu-latest
# steps:
# - name: Checkout code
# uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25.1'
cache: true
# - name: Set up Go
# uses: actions/setup-go@v5
# with:
# go-version: '1.25.1'
# cache: true

- name: Run go vet
run: go vet ./...
# - name: Run go vet
# run: go vet ./...

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v7
with:
version: v2.4.0
args: --timeout=5m
# - name: Run golangci-lint
# uses: golangci/golangci-lint-action@v7
# with:
# version: v2.4.0
# args: --timeout=5m

test:
name: Unit Test
Expand Down
176 changes: 176 additions & 0 deletions cmd/test-adapter/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"

"github.com/DevSymphony/sym-cli/internal/adapter"
"github.com/DevSymphony/sym-cli/internal/adapter/eslint"
"github.com/DevSymphony/sym-cli/internal/adapter/prettier"
"github.com/DevSymphony/sym-cli/internal/adapter/tsc"
)

// test-adapter is a CLI tool to test individual adapters
// Usage: go run cmd/test-adapter/main.go <adapter-name> [files...]
//
// Examples:
// go run cmd/test-adapter/main.go eslint src/app.js
// go run cmd/test-adapter/main.go tsc
// go run cmd/test-adapter/main.go prettier --check

func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: test-adapter <adapter-name> [files...]")
fmt.Println("\nAvailable adapters:")
fmt.Println(" - eslint")
fmt.Println(" - prettier")
fmt.Println(" - tsc")
fmt.Println("\nExamples:")
fmt.Println(" test-adapter eslint")
fmt.Println(" test-adapter tsc")
fmt.Println(" test-adapter prettier --check")
os.Exit(1)
}

adapterName := os.Args[1]
files := os.Args[2:]

// Get current directory
workDir, err := os.Getwd()
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}

symDir := filepath.Join(workDir, ".sym")
toolsDir := filepath.Join(os.Getenv("HOME"), ".sym", "tools")

// Create adapter
var adp adapter.Adapter
switch adapterName {
case "eslint":
adp = eslint.NewAdapter(toolsDir, workDir)
case "prettier":
adp = prettier.NewAdapter(toolsDir, workDir)
case "tsc":
adp = tsc.NewAdapter(toolsDir, workDir)
default:
fmt.Printf("Unknown adapter: %s\n", adapterName)
os.Exit(1)
}

fmt.Printf("🔧 Testing adapter: %s\n", adp.Name())
fmt.Printf("📁 Working directory: %s\n", workDir)
fmt.Printf("📁 .sym directory: %s\n", symDir)
fmt.Printf("📁 Tools directory: %s\n\n", toolsDir)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

// Check availability
fmt.Printf("🔍 Checking availability...\n")
if err := adp.CheckAvailability(ctx); err != nil {
fmt.Printf("⚠️ Not available: %v\n", err)
fmt.Printf("📦 Installing...\n")
if err := adp.Install(ctx, adapter.InstallConfig{
ToolsDir: toolsDir,
}); err != nil {
fmt.Printf("❌ Installation failed: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Installed successfully\n\n")
} else {
fmt.Printf("✅ Available\n\n")
}

// Load config from .sym directory
var config []byte
configPath := getConfigPath(symDir, adapterName)

fmt.Printf("📄 Looking for config: %s\n", configPath)
if data, err := os.ReadFile(configPath); err == nil {
config = data
fmt.Printf("✅ Using config from %s\n\n", configPath)
} else {
fmt.Printf("⚠️ No config found, using default\n\n")
config = []byte("{}")
}

// If no files specified, let the adapter use its default file discovery
if len(files) == 0 {
fmt.Printf("📂 No files specified, adapter will use its default file discovery\n\n")
} else {
fmt.Printf("📂 Files to check: %v\n\n", files)
}

// Execute adapter
fmt.Printf("🚀 Running %s...\n", adapterName)
output, err := adp.Execute(ctx, config, files)
if err != nil {
fmt.Printf("❌ Execution failed: %v\n", err)
os.Exit(1)
}

// Display results
fmt.Printf("\n📊 Results:\n")
fmt.Printf("Exit Code: %d\n", output.ExitCode)
fmt.Printf("Duration: %s\n\n", output.Duration)

if output.Stdout != "" {
fmt.Printf("📤 Stdout:\n%s\n\n", output.Stdout)
}

if output.Stderr != "" {
fmt.Printf("📤 Stderr:\n%s\n\n", output.Stderr)
}

// Parse violations
violations, err := adp.ParseOutput(output)
if err != nil {
fmt.Printf("⚠️ Failed to parse output: %v\n", err)
} else {
fmt.Printf("🔍 Found %d violation(s):\n\n", len(violations))
for i, v := range violations {
fmt.Printf("[%d] %s:%d:%d\n", i+1, v.File, v.Line, v.Column)
fmt.Printf(" Severity: %s\n", v.Severity)
fmt.Printf(" Message: %s\n", v.Message)
if v.RuleID != "" {
fmt.Printf(" Rule: %s\n", v.RuleID)
}
fmt.Printf("\n")
}
}

// Print summary as JSON
summary := map[string]interface{}{
"adapter": adapterName,
"exitCode": output.ExitCode,
"duration": output.Duration,
"violations": len(violations),
}

summaryJSON, _ := json.MarshalIndent(summary, "", " ")
fmt.Printf("\n📋 Summary:\n%s\n", string(summaryJSON))

// Exit with appropriate code
if output.ExitCode != 0 || len(violations) > 0 {
os.Exit(1)
}
}

func getConfigPath(symDir, adapterName string) string {
switch adapterName {
case "eslint":
return filepath.Join(symDir, ".eslintrc.json")
case "prettier":
return filepath.Join(symDir, ".prettierrc.json")
case "tsc":
return filepath.Join(symDir, "tsconfig.json")
default:
return filepath.Join(symDir, adapterName+".json")
}
}
10 changes: 5 additions & 5 deletions docs/CONVERT_FEATURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ sym convert -i user-policy.json -o code-policy.json

- `--targets`: Target linters (comma-separated or "all")
- `--output-dir`: Output directory for generated files
- `--openai-model`: OpenAI model to use (default: gpt-4o-mini)
- `--openai-model`: OpenAI model to use (default: gpt-4o)
- `--confidence-threshold`: Minimum confidence for inference (default: 0.7)
- `--timeout`: API call timeout in seconds (default: 30)
- `--verbose`: Enable verbose logging
Expand Down Expand Up @@ -182,7 +182,7 @@ sym convert -i user-policy.json -o code-policy.json
"input_file": "user-policy.json",
"total_rules": 5,
"targets": ["eslint", "checkstyle", "pmd"],
"openai_model": "gpt-4o-mini",
"openai_model": "gpt-4o",
"confidence_threshold": 0.7,
"linters": {
"eslint": {
Expand Down Expand Up @@ -362,19 +362,19 @@ go test ./internal/converter/linters/...
- Reduce number of rules
- Use caching (re-run with same rules)
- Increase `--timeout` for large rule sets
- Use faster OpenAI model (gpt-4o-mini)
- Use faster OpenAI model (gpt-4o)

## Performance

### Benchmarks (5 rules, no cache)

- **With LLM (gpt-4o-mini)**: ~5-10 seconds
- **With LLM (gpt-4o)**: ~5-10 seconds
- **Fallback only**: <1 second
- **With cache**: <100ms

### Cost Estimation

- **gpt-4o-mini**: ~$0.001 per rule
- **gpt-4o**: ~$0.001 per rule
- **gpt-4o**: ~$0.01 per rule
- **Caching**: Reduces cost by ~90% for repeated rules

Expand Down
6 changes: 3 additions & 3 deletions docs/CONVERT_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ Create a `user-policy.json` with natural language rules:
### Advanced Options

- `--output-dir`: Custom output directory (default: `<git-root>/.sym`)
- `--openai-model`: OpenAI model (default: `gpt-4o-mini`)
- `gpt-4o-mini` - Fast, cheap, good quality
- `--openai-model`: OpenAI model (default: `gpt-4o`)
- `gpt-4o` - Fast, cheap, good quality
- `gpt-4o` - Slower, more expensive, best quality
- `--confidence-threshold`: Minimum confidence (default: `0.7`)
- Range: 0.0 to 1.0
Expand Down Expand Up @@ -299,7 +299,7 @@ export OPENAI_API_KEY=sk-your-key-here
sym convert --targets all --timeout 60

# Use faster model (slightly less accurate)
sym convert --targets all --openai-model gpt-4o-mini
sym convert --targets all --openai-model gpt-4o

# Or split rules into smaller batches
```
Expand Down
2 changes: 1 addition & 1 deletion docs/LLM_VALIDATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Error: found 1 violation(s)

- `--policy, -p`: code-policy.json 경로 (기본: .sym/code-policy.json)
- `--staged`: staged 변경사항 검증
- `--model`: OpenAI 모델 (기본: gpt-4o-mini)
- `--model`: OpenAI 모델 (기본: gpt-4o)
- `--timeout`: 규칙당 타임아웃 (초, 기본: 30)

## 통합
Expand Down
5 changes: 1 addition & 4 deletions internal/adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,8 @@ type Adapter interface {
// Returns error if installation fails.
Install(ctx context.Context, config InstallConfig) error

// GenerateConfig generates tool-specific config from a rule.
// Returns config content (JSON, XML, YAML, etc.).
GenerateConfig(rule interface{}) ([]byte, error)

// Execute runs the tool with the given config and files.
// Config is read from .sym directory (e.g., .sym/.eslintrc.json).
// Returns raw tool output.
Execute(ctx context.Context, config []byte, files []string) (*ToolOutput, error)

Expand Down
4 changes: 0 additions & 4 deletions internal/adapter/checkstyle/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,6 @@ func (a *Adapter) Install(ctx context.Context, config adapter.InstallConfig) err
return nil
}

// GenerateConfig generates Checkstyle XML config from a rule.
func (a *Adapter) GenerateConfig(rule interface{}) ([]byte, error) {
return generateConfig(rule)
}

// Execute runs Checkstyle with the given config and files.
func (a *Adapter) Execute(ctx context.Context, config []byte, files []string) (*adapter.ToolOutput, error) {
Expand Down
Loading