From ca522df3460ac3c85ba028e2bfffed1820f016cc Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:45:09 +0000 Subject: [PATCH 01/13] refactor: remove unused helper functions and tests --- internal/util/config/config.go | 6 ---- internal/validator/llm_validator.go | 18 ----------- internal/validator/llm_validator_test.go | 5 --- internal/validator/validator_test.go | 41 ------------------------ 4 files changed, 70 deletions(-) diff --git a/internal/util/config/config.go b/internal/util/config/config.go index 552b0cf..bcc447f 100644 --- a/internal/util/config/config.go +++ b/internal/util/config/config.go @@ -63,9 +63,3 @@ func SaveConfig(cfg *Config) error { return os.WriteFile(configPath, data, 0600) } - - -// GetConfigPath returns the config file path -func GetConfigPath() string { - return configPath -} diff --git a/internal/validator/llm_validator.go b/internal/validator/llm_validator.go index 65d9c80..ee2a809 100644 --- a/internal/validator/llm_validator.go +++ b/internal/validator/llm_validator.go @@ -43,24 +43,6 @@ func newLLMValidator(provider llm.Provider, policy *schema.CodePolicy) *llmValid } } -// filterLLMRules filters rules that use llm-validator engine -func (v *llmValidator) filterLLMRules() []schema.PolicyRule { - llmRules := make([]schema.PolicyRule, 0) - - for _, rule := range v.policy.Rules { - if !rule.Enabled { - continue - } - - engine, ok := rule.Check["engine"].(string) - if ok && engine == "llm-validator" { - llmRules = append(llmRules, rule) - } - } - - return llmRules -} - // checkRule checks if code violates a specific rule using LLM // This is the single source of truth for LLM-based validation logic func (v *llmValidator) checkRule(ctx context.Context, change git.Change, addedLines []string, rule schema.PolicyRule) (*Violation, error) { diff --git a/internal/validator/llm_validator_test.go b/internal/validator/llm_validator_test.go index 799201c..9e25908 100644 --- a/internal/validator/llm_validator_test.go +++ b/internal/validator/llm_validator_test.go @@ -119,8 +119,3 @@ func TestExtractJSONField(t *testing.T) { } } -func TestFilterLLMRules(t *testing.T) { - // This would require creating a full CodePolicy and LLMValidator - // Skipping for now as it requires more setup - t.Skip("Integration test - requires full setup") -} diff --git a/internal/validator/validator_test.go b/internal/validator/validator_test.go index 60f9532..017305b 100644 --- a/internal/validator/validator_test.go +++ b/internal/validator/validator_test.go @@ -243,47 +243,6 @@ func TestFilterChangesForRule(t *testing.T) { }) } -func TestFilterLLMRules_Detailed(t *testing.T) { - t.Run("filters llm-validator rules only", func(t *testing.T) { - policy := &schema.CodePolicy{ - Rules: []schema.PolicyRule{ - {ID: "rule1", Enabled: true, Check: map[string]interface{}{"engine": "llm-validator"}}, - {ID: "rule2", Enabled: true, Check: map[string]interface{}{"engine": "eslint"}}, - {ID: "rule3", Enabled: true, Check: map[string]interface{}{"engine": "llm-validator"}}, - }, - } - v := &llmValidator{policy: policy} - result := v.filterLLMRules() - assert.Len(t, result, 2) - assert.Equal(t, "rule1", result[0].ID) - assert.Equal(t, "rule3", result[1].ID) - }) - - t.Run("skips disabled rules", func(t *testing.T) { - policy := &schema.CodePolicy{ - Rules: []schema.PolicyRule{ - {ID: "rule1", Enabled: false, Check: map[string]interface{}{"engine": "llm-validator"}}, - {ID: "rule2", Enabled: true, Check: map[string]interface{}{"engine": "llm-validator"}}, - }, - } - v := &llmValidator{policy: policy} - result := v.filterLLMRules() - assert.Len(t, result, 1) - assert.Equal(t, "rule2", result[0].ID) - }) - - t.Run("returns empty for no matching rules", func(t *testing.T) { - policy := &schema.CodePolicy{ - Rules: []schema.PolicyRule{ - {ID: "rule1", Enabled: true, Check: map[string]interface{}{"engine": "eslint"}}, - }, - } - v := &llmValidator{policy: policy} - result := v.filterLLMRules() - assert.Len(t, result, 0) - }) -} - func TestLinterExecutionUnit_Getters(t *testing.T) { rules := []schema.PolicyRule{ {ID: "rule-1"}, From baa7c8fce83848025b2cccaa637cf5a155601ee0 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:45:18 +0000 Subject: [PATCH 02/13] chore: simplify Makefile build targets --- Makefile | 84 ++++++++------------------------------------------------ 1 file changed, 11 insertions(+), 73 deletions(-) diff --git a/Makefile b/Makefile index 940fd31..3120a7c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ -# symphonyclient integration: Added CSS build targets -.PHONY: build test install clean fmt lint tidy help build-all setup coverage-check build-css install-css watch-css +.PHONY: build build-all test unit-test clean fmt lint tidy setup run help BINARY_NAME=sym BUILD_DIR=bin @@ -9,116 +8,55 @@ LDFLAGS=-ldflags "-X main.Version=$(VERSION)" help: @echo "Available targets:" - @echo " build - Build the binary for current platform (includes CSS)" + @echo " build - Build the binary for current platform" @echo " build-all - Build for all platforms (Linux, macOS, Windows)" - @echo " build-css - Build Tailwind CSS for dashboard" - @echo " watch-css - Watch and rebuild CSS on changes" - @echo " test - Run tests" - @echo " install - Install the binary to GOPATH/bin" + @echo " test - Run tests with coverage" + @echo " unit-test - Run tests without coverage" @echo " clean - Remove build artifacts" @echo " fmt - Format code" @echo " lint - Run linter" @echo " tidy - Tidy dependencies" @echo " setup - Setup development environment" + @echo " run - Run the application" -# symphonyclient integration: CSS build for dashboard -install-css: - @echo "Installing CSS dependencies..." - @npm install - -build-css: install-css - @echo "Building Tailwind CSS..." - @npm run build:css - @echo "CSS build complete" - -watch-css: install-css - @echo "Watching CSS changes..." - @npm run watch:css - -build: build-css - @echo "Building $(BINARY_NAME)..." +build: @mkdir -p $(BUILD_DIR) ifeq ($(OS),Windows_NT) @go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME).exe $(MAIN_PATH) - @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME).exe" else @go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(MAIN_PATH) - @echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)" endif -build-all: build-css - @echo "Building for all platforms..." +build-all: @mkdir -p $(BUILD_DIR) - @echo "Building Linux amd64..." @GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 $(MAIN_PATH) - @echo "Building Linux arm64..." @GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 $(MAIN_PATH) - @echo "Building macOS amd64..." @GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 $(MAIN_PATH) - @echo "Building macOS arm64..." @GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 $(MAIN_PATH) - @echo "Building Windows amd64..." @GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH) - @echo "All platform builds complete" test: - @echo "Running tests..." @go test -v -race -coverprofile=coverage.out ./... - @go tool cover -html=coverage.out -o coverage.html - @echo "Test complete. Coverage report: coverage.html" -coverage-check: - @echo "Checking coverage threshold..." - @go test -coverprofile=coverage.out ./... > /dev/null 2>&1 - @COVERAGE=$$(go tool cover -func=coverage.out | grep total | awk '{print $$3}' | sed 's/%//'); \ - THRESHOLD=80; \ - echo "Current coverage: $$COVERAGE%"; \ - echo "Required threshold: $$THRESHOLD%"; \ - if [ "$$(echo "$$COVERAGE < $$THRESHOLD" | bc -l 2>/dev/null || awk "BEGIN {print ($$COVERAGE < $$THRESHOLD)}")" -eq 1 ]; then \ - echo "❌ Coverage $$COVERAGE% is below threshold $$THRESHOLD%"; \ - exit 1; \ - else \ - echo "✅ Coverage $$COVERAGE% meets threshold $$THRESHOLD%"; \ - fi - -install: - @echo "Installing $(BINARY_NAME)..." - @go install $(MAIN_PATH) - @echo "Install complete" +unit-test: + @go test -short ./... clean: - @echo "Cleaning..." @rm -rf $(BUILD_DIR) @rm -f coverage.out coverage.html - @rm -rf node_modules - @echo "Clean complete" fmt: - @echo "Formatting code..." @go fmt ./... - @echo "Format complete" lint: - @echo "Running linter..." - golangci-lint run - @echo "Linter complete" + @golangci-lint run tidy: - @echo "Tidying dependencies..." @go mod tidy - @echo "Tidy complete" -# Development helpers -setup: tidy install-css - @echo "Setting up development environment..." +setup: @go mod download @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 - @echo "Development environment setup complete" - -dev-deps: - @echo "Installing development dependencies..." - @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - @echo "Development dependencies installed" run: @go run $(MAIN_PATH) $(ARGS) From d245d74a65dc7444d9676e0ff18680b14ae49436 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:45:28 +0000 Subject: [PATCH 03/13] docs: add architecture and contributing guides --- docs/ARCHITECTURE.md | 311 ++++++++++++++++++++++ docs/CONTRIBUTING.md | 618 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 929 insertions(+) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CONTRIBUTING.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b1088ff --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,311 @@ +# Architecture + +Symphony CLI의 전체 아키텍처와 패키지 의존성 구조를 설명합니다. + +## Overview + +Symphony는 LLM 기반 코드 컨벤션 검사 도구로, 다음과 같은 핵심 기능을 제공합니다: + +- 자연어로 컨벤션 정의 +- LLM이 MCP를 통해 필요한 컨벤션만 추출하여 컨텍스트에 포함 +- LLM이 MCP를 통해 코드 변경사항에 대한 컨벤션 준수 여부를 검사 +- RBAC 기반 접근 제어 + +## Layer Structure + +시스템은 8개의 논리적 계층으로 구성됩니다: + +| Layer | Name | Role | +|-------|------|------| +| 0 | Bootstrap | 애플리케이션 진입점, 프로바이더 등록 | +| 1 | Commands | CLI 명령어 구현 | +| 2 | Gateways | 외부 인터페이스 (MCP stdio, HTTP) | +| 3 | Core | 핵심 비즈니스 로직 (변환, 검증) | +| 4 | Tool Adapters | 린터 및 LLM 프로바이더 어댑터 | +| 5 | Policy & Access | 정책 관리 및 접근 제어 | +| 6 | Utilities | 공통 유틸리티 | +| 7 | Contracts | 공유 데이터 스키마 | + +## Dependency Diagram + +### Simple Version + +```mermaid +graph TD + subgraph L0["0 Bootstrap"] + main["cmd/sym"] + end + + subgraph L1["1 Commands"] + cmd["internal/cmd"] + end + + subgraph L2["2 Gateways"] + mcp["mcp
(MCP stdio)"] + server["server
(HTTP)"] + end + + subgraph L3["3 Core"] + converter["converter"] + validator["validator"] + end + + subgraph L4["4 Tool Adapters"] + linter["linter/*"] + llm["llm/*"] + end + + subgraph L5["5 Policy & Access"] + policy["policy"] + roles["roles"] + end + + subgraph L6["6 Utilities"] + config["util/config"] + env["util/env"] + git["util/git"] + end + + subgraph L7["7 Contracts"] + schema["pkg/schema"] + end + + main --> cmd + main -.->|register| linter + main -.->|register| llm + + cmd --> L2 + cmd --> L3 + cmd --> L4 + cmd --> L5 + cmd --> L6 + cmd --> schema + + L2 --> L3 + L2 --> L4 + L2 --> L5 + L2 --> L6 + L2 --> schema + + L3 --> L4 + L3 --> L5 + L3 --> L6 + L3 --> schema + + L4 --> L6 + L4 --> schema + + L5 --> L6 + L5 --> schema +``` + +## Layer Details + +### Layer 0: Bootstrap (`cmd/sym`) + +애플리케이션 진입점입니다. `main.go`에서 CLI를 실행하고, `bootstrap.go`에서 린터와 LLM 프로바이더를 레지스트리에 등록합니다. + +```go +// cmd/sym/bootstrap.go +import ( + _ "github.com/DevSymphony/sym-cli/internal/linter/eslint" + _ "github.com/DevSymphony/sym-cli/internal/llm/claudecode" + // ... +) +``` + +### Layer 1: Commands (`internal/cmd`) + +Cobra 프레임워크 기반 CLI 명령어를 구현합니다. + +| Command | Description | +|---------|-------------| +| `sym init` | 프로젝트 초기화 | +| `sym validate` | 코드 검증 | +| `sym convert` | 정책 변환 | +| `sym dashboard` | 웹 대시보드 실행 | +| `sym mcp` | MCP 서버 실행 | +| `sym my-role` | 역할 관리 | +| `sym llm status\|test\|setup` | LLM 프로바이더 관리 | + +### Layer 2: Gateways + +외부 시스템과의 인터페이스를 제공합니다. + +#### MCP Server (`internal/mcp`) + +AI 코딩 도구(Claude Code, Cursor 등)와 stdio를 통해 통신합니다. + +| Tool | Description | +|------|-------------| +| `query_conventions` | 프로젝트 컨벤션 조회 | +| `validate_code` | 코드 변경사항 검증 | + +#### HTTP Server (`internal/server`) + +웹 대시보드용 REST API를 제공합니다. + +| Endpoint | Description | +|----------|-------------| +| `GET /api/policy` | 정책 조회 | +| `POST /api/policy` | 정책 저장 | +| `GET /api/roles` | 역할 목록 조회 | +| `POST /api/select-role` | 역할 선택 | + +### Layer 3: Core + +핵심 비즈니스 로직을 구현합니다. + +#### Converter (`internal/converter`) + +UserPolicy(A Schema)를 CodePolicy(B Schema)로 변환합니다. + +``` +UserPolicy (자연어 규칙) + | + v LLM 라우팅 ++---------------------------+ +| 규칙별 린터 매핑 | +| - ESLint 규칙 | +| - Prettier 설정 | +| - llm-validator fallback | ++---------------------------+ + | + v +CodePolicy (정형화된 규칙) + 린터 설정 파일 +``` + +#### Validator (`internal/validator`) + +4단계 파이프라인으로 코드를 검증합니다: + +1. **RBAC 검사**: 역할 기반 파일 접근 권한 확인 +2. **규칙 그룹화**: 엔진별로 규칙 분류 +3. **실행 단위 생성**: 린터/LLM 실행 단위 구성 +4. **병렬 실행**: 세마포어 기반 동시성 제어 + +### Layer 4: Tool Adapters + +외부 도구와의 통합을 담당합니다. +Registry Pattern을 사용하므로 새로운 도구를 추가할 때 기존 코드 수정 불필요합니다. + +#### Linter (`internal/linter/*`) + +정적 린팅 도구 어댑터입니다. 각 린터는 `Linter`와 `Converter` 인터페이스를 구현합니다. + +| Linter | Language | Purpose | +|--------|----------|---------| +| ESLint | JS/TS | 코드 품질, 스타일 | +| Prettier | JS/TS/JSON/CSS | 코드 포맷팅 | +| Pylint | Python | 코드 품질 | +| TSC | TypeScript | 타입 검사 | +| Checkstyle | Java | 스타일 검사 | +| PMD | Java | 정적 분석 | + +#### LLM (`internal/llm/*`) + +LLM 프로바이더 어댑터입니다. 각 프로바이더는 `RawProvider` 인터페이스를 구현합니다. + +| Provider | Type | Default Model | +|----------|------|---------------| +| claudecode | CLI | sonnet | +| geminicli | CLI | gemini-2.5-flash | +| openaiapi | API | gpt-4o-mini | + +### Layer 5: Policy & Access + +정책 관리와 접근 제어를 담당합니다. + +#### Policy (`internal/policy`) + +정책 파일의 로드, 저장, 검증 및 템플릿 관리를 수행합니다. + +- `user-policy.json` (A Schema) 관리 +- `code-policy.json` (B Schema) 관리 +- 내장 템플릿 제공 (React, Vue, Node.js, Python, Go, TypeScript) + +#### Roles (`internal/roles`) + +RBAC 기반 접근 제어를 제공합니다. + +- 역할 정의 관리 (`roles.json`) +- 현재 역할 상태 관리 (`.sym/.env`) +- 파일 접근 권한 검증 (AllowWrite/DenyWrite 패턴) + +### Layer 6: Utilities (`internal/util/*`) + +공통 유틸리티를 제공합니다. + +| Package | Description | +|---------|-------------| +| `util/config` | 프로젝트 설정 (`config.json`) 관리 | +| `util/env` | 환경 변수 파일 (`.env`) 관리 | +| `util/git` | Git 저장소 정보, diff 추출 | + +### Layer 7: Contracts (`pkg/schema`) + +시스템 전체에서 공유하는 데이터 스키마를 정의합니다. + +``` ++---------------------------------------------+ +| A Schema (사용자 친화적) | +| UserPolicy --+-- UserRBAC --- UserRole | +| +-- UserDefaults | +| +-- UserRule[] | ++------------------------+--------------------+ + | + converter.Convert() + | + v ++---------------------------------------------+ +| B Schema (시스템 실행용) | +| CodePolicy --+-- PolicyRBAC --- PolicyRole | +| +-- PolicyRule[] | +| +-- EnforceSettings | ++---------------------------------------------+ +``` + +## Design Principles + +### Unidirectional Dependencies + +상위 계층은 하위 계층만 의존합니다. 순환 의존성이 없습니다. + +``` +Bootstrap -> Commands -> Gateways -> Core -> Adapters -> Utilities -> Contracts +``` + +### Registry Pattern + +린터와 LLM 프로바이더는 `init()` 함수에서 전역 레지스트리에 등록됩니다. 이를 통해: + +- 새 어댑터 추가 시 기존 코드 수정 불필요 +- Bootstrap에서 import만으로 등록 완료 +- 런타임에 사용 가능한 도구 동적 조회 + +### Interface-based Design + +핵심 컴포넌트는 인터페이스로 정의되어 테스트 용이성과 확장성을 보장합니다. + +```go +// Linter interface +type Linter interface { + Name() string + Execute(ctx context.Context, config []byte, files []string) (*ToolOutput, error) + ParseOutput(output *ToolOutput) ([]Violation, error) +} + +// LLM Provider interface +type RawProvider interface { + Name() string + ExecuteRaw(ctx context.Context, prompt string, format ResponseFormat) (string, error) + Close() error +} +``` + +## References + +- [CLI Command Reference](COMMAND.md) +- [pkg/schema README](../pkg/schema/README.md) - Schema definitions +- [internal/linter README](../internal/linter/README.md) - Linter integration guide +- [internal/llm README](../internal/llm/README.md) - LLM provider integration guide diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000..d238460 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,618 @@ +# Symphony CLI 기여 가이드 + +Symphony CLI에 기여해 주셔서 감사합니다. 이 가이드는 시작하는 데 도움이 될 것입니다. + +## 목차 + +- [개발 환경 설정](#개발-환경-설정) +- [프로젝트 구조](#프로젝트-구조) +- [패키지 README.md 가이드](#패키지-readmemd-가이드) +- [커밋 컨벤션](#커밋-컨벤션) +- [Pull Request 가이드](#pull-request-가이드) +- [확장 가이드](#확장-가이드) +- [테스트 가이드](#테스트-가이드) +- [참고 문서](#참고-문서) + +--- + +## 개발 환경 설정 + +이 섹션에서는 Symphony CLI에 기여하기 위한 개발 환경 설정 방법을 설명합니다. + +### 요구 사항 + +Symphony CLI를 빌드하고 실행하려면 다음 도구가 필요합니다: + +| 도구 | 버전 | 설명 | +|------|------|------| +| Go | 1.25.1 | 프로그래밍 언어 | +| Git | 최신 | 버전 관리 | + +### DevContainer (권장) + +일관되고 미리 구성된 개발 환경을 위해 VS Code DevContainer 사용을 권장합니다. DevContainer에는 모든 필수 종속성과 도구가 포함되어 있습니다. + +**사용 방법:** + +1. [Docker](https://www.docker.com/)와 [VS Code](https://code.visualstudio.com/) 설치 +2. VS Code에서 [Dev Containers 확장](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 설치 +3. VS Code에서 프로젝트 폴더 열기 +4. `Ctrl+Shift+P` (macOS에서는 `Cmd+Shift+P`)를 누르고 **"Dev Containers: Reopen in Container"** 선택 +5. 컨테이너 빌드 대기 (처음에는 몇 분 정도 소요될 수 있음) + +DevContainer는 Ubuntu 24.04 기반이며 다음을 포함합니다: +- Go 1.25.1 +- Git +- golangci-lint (`make setup`을 통해 설치) + +컨테이너가 시작되면 `post-create.sh` 스크립트가 자동으로 `make setup`을 실행합니다. + +### 로컬 설정 + +DevContainer 없이 로컬에서 개발하려면 다음 단계를 따르세요: + +```bash +# 1. 저장소 클론 +git clone https://github.com/DevSymphony/sym-cli.git +cd sym-cli + +# 2. 개발 환경 설정 +# Go 종속성 다운로드 및 golangci-lint 설치 +make setup + +# 3. 빌드 확인 +make build + +# 4. 테스트 실행하여 모든 것이 작동하는지 확인 +make test +``` + +### Make 타겟 + +프로젝트는 일반적인 개발 작업에 Make를 사용합니다. `make help`를 실행하여 사용 가능한 모든 타겟을 확인할 수 있습니다. + +| 타겟 | 설명 | +|------|------| +| `make setup` | 개발 환경 초기화: Go 종속성 다운로드 및 golangci-lint v2.4.0 설치 | +| `make build` | 현재 플랫폼용 바이너리 빌드. 출력: `bin/sym` | +| `make build-all` | 모든 플랫폼용 빌드 (Linux amd64/arm64, macOS amd64/arm64, Windows amd64) | +| `make test` | 레이스 감지와 함께 모든 테스트 실행 및 커버리지 리포트 생성 (`coverage.out`) | +| `make unit-test` | 커버리지 없이 짧은 모드로 테스트 실행 (빠른 확인용) | +| `make lint` | golangci-lint를 실행하여 코드 품질 검사 | +| `make fmt` | `go fmt`를 사용하여 모든 Go 소스 파일 포맷팅 | +| `make tidy` | `go mod tidy`를 실행하여 모듈 종속성 정리 | +| `make clean` | 빌드 아티팩트 제거 (`bin/`, `coverage.out`, `coverage.html`) | +| `make run ARGS="..."` | 지정된 인자로 애플리케이션 빌드 및 실행 | + +**일반적인 워크플로우:** + +```bash +# 커밋 전: 포맷팅, 린트, 테스트 +make fmt && make lint && make test + +# 개발 중 빠른 반복 +make build && ./bin/sym --help + +# 특정 명령 실행 +make run ARGS="validate --help" +``` + +--- + +## 프로젝트 구조 + +프로젝트 구조를 이해하면 코드베이스를 탐색하고 어디서 변경해야 하는지 파악하는 데 도움이 됩니다. + +``` +sym-cli/ +├── cmd/sym/ # 진입점 +│ ├── main.go # CLI 실행 - 루트 명령 초기화 및 실행 +│ └── bootstrap.go # 린터/LLM 프로바이더 등록을 위한 사이드 이펙트 임포트 +│ +├── internal/ # 비공개 패키지 (외부 프로젝트에서 임포트 불가) +│ ├── cmd/ # Cobra로 구현된 CLI 명령 +│ ├── converter/ # 사용자 정책(Schema A)을 코드 정책(Schema B)으로 변환 +│ ├── validator/ # 정책에 대한 코드 검증 조율 +│ ├── linter/ # 통합 린터 인터페이스 및 구현체 +│ │ ├── eslint/ # JavaScript/TypeScript용 ESLint +│ │ ├── prettier/ # 코드 포맷팅용 Prettier +│ │ ├── pylint/ # Python용 Pylint +│ │ ├── tsc/ # 타입 검사용 TypeScript 컴파일러 +│ │ ├── checkstyle/ # Java용 Checkstyle +│ │ └── pmd/ # Java 정적 분석용 PMD +│ ├── llm/ # 통합 LLM 프로바이더 인터페이스 +│ │ ├── claudecode/ # Claude Code CLI 프로바이더 +│ │ ├── geminicli/ # Gemini CLI 프로바이더 +│ │ └── openaiapi/ # OpenAI API 프로바이더 +│ ├── mcp/ # AI 도구 통합을 위한 Model Context Protocol 서버 +│ ├── server/ # 웹 대시보드 HTTP 서버 +│ ├── policy/ # 정책 파일 로드, 저장 및 템플릿 관리 +│ ├── roles/ # RBAC (역할 기반 접근 제어) 관리 +│ └── util/ # 공유 유틸리티 +│ ├── config/ # 설정 파일 관리 +│ ├── env/ # 환경 변수 및 .env 파일 처리 +│ └── git/ # Git 작업 (diff, 변경된 파일 감지) +│ +├── pkg/ # 공개 패키지 (외부 프로젝트에서 임포트 가능) +│ └── schema/ # 정책 스키마 타입 정의 (UserPolicy, CodePolicy) +│ +├── docs/ # 문서 +│ ├── COMMAND.md # CLI 명령 참조 +│ ├── server-api.md # REST API 문서 +│ └── CONTRIBUTING.md # 이 파일 +│ +└── tests/ # 테스트 픽스처 및 통합 테스트 + └── testdata/ # 테스트 데이터 파일 +``` + +**주요 아키텍처 개념:** + +- **Schema A (UserPolicy)**: 자연어 규칙이 포함된 사람 친화적인 정책 형식 +- **Schema B (CodePolicy)**: 검증 엔진용 기계 판독 가능한 정책 형식 +- **Converter**: 규칙 해석을 위해 LLM을 사용하여 Schema A → Schema B 변환 +- **Validator**: 린터 및 LLM 기반 검증을 통해 변환된 규칙 실행 +- **Registry 패턴**: 린터와 LLM 프로바이더가 `init()` 함수를 통해 자체 등록 + +--- + +## 패키지 README.md 가이드 + +모든 패키지에는 목적, 구조 및 API를 문서화하는 README.md가 있어야 합니다. 이는 다른 기여자가 모든 코드를 읽지 않고도 패키지를 이해하는 데 도움이 됩니다. + +### 섹션 설명 + +각 README.md에는 다음 섹션이 포함되어야 합니다: + +#### 1. 패키지 이름 및 설명 + +패키지 이름을 제목으로 시작하고, 패키지가 하는 일에 대한 한 줄 설명을 추가합니다. + +#### 2. 패키지 구조 + +각 파일의 역할에 대한 간단한 설명과 함께 디렉토리 트리를 표시합니다. 이를 통해 독자는 패키지가 어떻게 구성되어 있는지 빠르게 파악할 수 있습니다. + +#### 3. 의존성 + +**패키지 사용자**: 이 패키지를 임포트하는 모든 다른 패키지를 나열합니다. 파일 경로와 *왜* 사용하는지 설명을 포함합니다. 이는 변경 시 무엇이 깨질 수 있는지 아는 데 도움이 됩니다. + +**패키지 의존성**: 이 패키지가 임포트하는 모든 내부 패키지를 나열합니다. 표준 라이브러리나 외부 종속성이 아닌, 이 저장소의 패키지만 포함합니다. + +#### 4. Public/Private API + +**Public API**: 내보낸(대문자로 시작하는) 모든 타입, 함수 및 메서드를 문서화합니다. 포함할 내용: +- 타입 이름과 정의 파일 +- 함수 시그니처 +- 각각이 하는 일에 대한 간단한 설명 + +**Private API**: 주요 내부 함수를 나열합니다. 전체 시그니처는 필요 없습니다 - 함수 이름과 목적에 대한 간단한 설명만 포함합니다. + +#### 5. 참고 문헌 - 선택 사항 + +해당되는 경우 외부 문서, 관련 스펙 또는 설계 문서 링크를 포함합니다. + +### 템플릿 + +새 패키지 README를 작성할 때 이 템플릿을 사용하세요: + +````markdown +# 패키지 이름 + +패키지의 목적과 기능에 대한 간결한 설명. + +## 패키지 구조 + +``` +package/ +├── file1.go # file1에 대한 간단한 설명 +├── file2.go # file2에 대한 간단한 설명 +├── subpackage/ # 서브패키지에 대한 간단한 설명 +│ └── ... +└── file_test.go # 테스트 +``` + +## 의존성 + +### 패키지 사용자 + +이 패키지를 임포트하는 모든 패키지와 사용 목적을 나열합니다. + +| 위치 | 목적 | +|------|------| +| `internal/cmd/validate.go` | 정책 규칙에 대한 코드 검사에 validator 사용 | +| `internal/mcp/server.go` | MCP 도구를 통해 검증 노출 | + +### 패키지 의존성 + +이 패키지가 의존하는 모든 내부 패키지를 나열합니다. + +``` +this-package +├── internal/linter # 정적 분석 도구 실행용 +├── internal/llm # LLM 기반 검증용 +├── internal/roles # RBAC 권한 검사용 +└── pkg/schema # 정책 타입 정의용 +``` + +## Public/Private API + +### Public API + +#### 타입 + +| 타입 | 파일 | 설명 | +|------|------|------| +| `Validator` | validator.go | 메인 검증기 조율자 | +| `ValidationResult` | validator.go | 위반 사항 및 메타데이터 포함 | + +#### 함수 + +| 함수 | 시그니처 | 설명 | +|------|----------|------| +| `NewValidator` | `func NewValidator(policy *schema.CodePolicy, verbose bool) *Validator` | 새 검증기 인스턴스 생성 | +| `ValidateChanges` | `func (v *Validator) ValidateChanges(ctx context.Context, changes []Change) (*ValidationResult, error)` | 정책에 대한 코드 변경 검증 | + +### Private API + +외부 사용을 위해 내보내지 않는 내부 함수: + +- `groupRulesByEngine()` - 검증 엔진 타입별 규칙 그룹화 +- `buildExecutionUnits()` - 병렬 실행 단위 생성 +- `mergeResults()` - 여러 검증기의 결과 결합 + +### 참고 문헌 + +- [외부 도구 문서](https://example.com) +- 관련 내부 문서 +```` + +--- + +## 커밋 컨벤션 + +우리는 간소화된 Conventional Commits 형식을 따릅니다. 일관된 커밋 메시지는 프로젝트 히스토리를 이해하고 변경 로그를 생성하기 쉽게 만듭니다. + +### 형식 + +``` +: + +[선택적 본문] +``` + +- **type**: 변경 카테고리 (아래 표 참조) +- **subject**: 변경 내용의 짧은 설명 (명령형, 소문자, 마침표 없음) +- **body**: 선택적 상세 설명 (제목만으로 충분하지 않을 때) + +### 커밋 타입 + +| 타입 | 사용 시점 | 예시 | +|------|----------|------| +| `feat` | 새 기능 추가 | `feat: add OpenAI provider support` | +| `fix` | 버그 수정 | `fix: handle nil pointer in validator` | +| `docs` | 문서만 변경 | `docs: update API reference` | +| `style` | 포맷팅, 공백 (코드 변경 없음) | `style: fix indentation in converter` | +| `refactor` | 버그 수정도 기능 추가도 아닌 코드 변경 | `refactor: extract git operations to util package` | +| `test` | 테스트 추가 또는 업데이트 | `test: add unit tests for RBAC validation` | +| `chore` | 빌드 프로세스, 종속성, 도구 | `chore: update golangci-lint to v2.4.0` | + +### 규칙 + +1. **제목 줄**: 소문자로 시작, 끝에 마침표 없음, 50자 이내 +2. **명령형**: "add feature"로 작성, "added feature"나 "adds feature"가 아님 +3. **본문**: 변경 사항이 제목 이상의 설명을 필요로 할 때 사용 +4. **범위**: 범위(예: `feat(llm):`)를 사용하지 않음 - 단순하게 유지 + +### 예시 + +**간단한 커밋 (제목만):** +``` +feat: add retry logic for API calls +``` + +**본문이 있는 커밋 (복잡한 변경용):** +``` +feat: add exponential backoff for LLM API calls + +- Retry up to 3 times with exponential backoff +- Initial delay: 1 second, max delay: 10 seconds +- Configurable via LLM_RETRY_COUNT environment variable +``` + +**버그 수정:** +``` +fix: prevent panic when policy file is empty + +Return early with descriptive error instead of attempting +to parse nil content. +``` + +--- + +## Pull Request 가이드 + +이 섹션에서는 이 프로젝트의 pull request 생성 및 작성 방법을 설명합니다. + +### PR 제목 + +PR 제목은 커밋 메시지와 동일한 형식을 따라야 합니다: + +``` +: +``` + +"Squash and Merge" 사용 시 제목이 커밋 메시지가 되므로 설명적으로 작성하세요. + +**좋은 예시:** +- `feat: add Gemini CLI provider` +- `fix: resolve race condition in parallel validation` +- `docs: add contribution guide` +- `refactor: simplify linter registry initialization` + +**나쁜 예시:** +- `Update code` (너무 모호함) +- `Fixed bug` (타입 접두사 누락, 설명적이지 않음) +- `feat: Add New Feature for Better Performance` (모든 단어를 대문자로 하지 않음) + +### PR 본문 형식 + +PR 설명에 이 템플릿을 사용하세요: + +```markdown +## Summary +- 이 PR이 무엇을 하고 왜 하는지에 대한 간단한 설명 +- "어떻게"가 아닌 "무엇"과 "왜"에 집중 + +## Changes +- 수행된 구체적인 변경 사항 목록 +- 한 줄에 하나씩 +- 영향받는 파일이나 컴포넌트를 구체적으로 명시 +``` + +**Summary에 포함할 내용:** +- 해결하는 문제 또는 추가하는 기능 +- 이 변경이 필요한 이유 +- 리뷰어가 알아야 할 중요한 컨텍스트 + +**Changes에 포함할 내용:** +- 추가된 새 파일 또는 패키지 +- 수정된 기능 +- 삭제되거나 더 이상 사용되지 않는 코드 +- 설정 변경 + +### PR 예시 + +**제목:** `feat: add Claude API provider` + +**본문:** +```markdown +## Summary +- Add support for Claude API as a new LLM provider +- Enables users without Claude CLI to use the API directly +- Includes automatic retry with exponential backoff + +## Changes +- Add `internal/llm/claudeapi/` package with provider implementation +- Implement `RawProvider` interface with streaming support +- Add API key validation in provider initialization +- Register provider in `cmd/sym/bootstrap.go` +- Add unit tests for provider (85% coverage) +- Update `internal/llm/README.md` with new provider documentation +``` + +### 병합 전략 + +모든 pull request에 **Squash and Merge**를 사용합니다. + +**의미:** +- PR의 모든 커밋이 단일 커밋으로 결합됨 +- PR 제목이 최종 커밋 메시지가 됨 +- 개별 커밋 히스토리는 PR에 보존되지만 main 브랜치에는 포함되지 않음 + +**이유:** +- main 브랜치 히스토리를 깔끔하고 읽기 쉽게 유지 +- 각 PR = 하나의 논리적 변경 = 하나의 커밋 +- 필요시 전체 기능을 쉽게 되돌릴 수 있음 + +**병합 전 확인사항:** +1. 모든 CI 검사 통과 +2. 코드 리뷰 및 승인 완료 +3. PR 제목이 커밋 컨벤션을 따름 +4. PR 설명이 완성됨 + +--- + +## 확장 가이드 + +Symphony CLI는 확장 가능하게 설계되었습니다. 새로운 LLM 프로바이더나 린터를 추가할 수 있습니다. + +### LLM 프로바이더 추가 + +새 프로바이더를 추가하려면: +1. `internal/llm//` 패키지 생성 +2. `RawProvider` 인터페이스 구현 +3. `init()`에서 `llm.RegisterProvider()` 호출 +4. `cmd/sym/bootstrap.go`에 빈 임포트 추가 + +상세 가이드와 코드 예제는 [`internal/llm/README.md`](../internal/llm/README.md)를 참조하세요. + +### 린터 추가 + +새 린터를 추가하려면: +1. `internal/linter//` 패키지 생성 +2. `Linter` 인터페이스 구현 (도구 실행) +3. `Converter` 인터페이스 구현 (규칙 변환) +4. `init()`에서 `linter.Global().RegisterTool()` 호출 +5. `cmd/sym/bootstrap.go`에 빈 임포트 추가 + +상세 가이드와 코드 예제는 [`internal/linter/README.md`](../internal/linter/README.md)를 참조하세요. + +--- + +## 테스트 가이드 + +이 섹션에서는 Symphony CLI의 테스트 작성 및 실행 방법을 설명합니다. + +### 테스트 실행 + +Make 타겟을 사용하여 테스트 실행: + +```bash +# 레이스 감지 및 커버리지와 함께 모든 테스트 실행 +# coverage.out 파일 생성 +make test + +# 커버리지 없이 빠른 테스트 실행 (개발 반복용으로 더 빠름) +make unit-test + +# 특정 패키지 테스트 실행 +go test -v ./internal/validator/... + +# 특정 테스트 함수 실행 +go test -v ./internal/validator/... -run TestValidateChanges + +# 자세한 출력과 캐싱 없이 테스트 실행 +go test -v -count=1 ./... +``` + +### 테스트 작성 가이드라인 + +테스트 작성 시 다음 가이드라인을 따르세요: + +1. **파일 명명**: 테스트 파일은 `_test.go`로 끝나야 함 +2. **함수 명명**: 테스트 함수는 `Test`로 시작해야 함 (예: `TestValidateChanges`) +3. **패키지**: 테스트는 테스트 대상 코드와 같은 패키지에 있어야 함 +4. **어설션**: 읽기 쉬운 어설션을 위해 `github.com/stretchr/testify` 사용 + +### 테스트 구조 + +명확한 테스트 구조를 위해 Arrange-Act-Assert 패턴 사용: + +```go +func TestValidator_ValidateChanges(t *testing.T) { + // Arrange: 테스트 데이터 및 종속성 설정 + policy := &schema.CodePolicy{ + Rules: []schema.Rule{ + {ID: "1", Category: "naming"}, + }, + } + validator := NewValidator(policy, false) + changes := []Change{ + {File: "test.go", Content: "package main"}, + } + + // Act: 테스트 대상 코드 실행 + result, err := validator.ValidateChanges(context.Background(), changes) + + // Assert: 결과 검증 + require.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result.Violations) +} +``` + +### 테이블 기반 테스트 + +여러 시나리오 테스트에 테이블 기반 테스트 사용: + +```go +func TestParseOutput(t *testing.T) { + tests := []struct { + name string + input string + expected []Violation + wantErr bool + }{ + { + name: "유효한 JSON 출력", + input: `[{"file": "test.go", "line": 10, "message": "error"}]`, + expected: []Violation{{File: "test.go", Line: 10, Message: "error"}}, + wantErr: false, + }, + { + name: "빈 출력", + input: `[]`, + expected: []Violation{}, + wantErr: false, + }, + { + name: "잘못된 JSON", + input: `not json`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseOutput(tt.input) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} +``` + +### 모킹 + +종속성 모킹이 필요한 단위 테스트용: + +```go +// 종속성용 인터페이스 정의 +type LLMProvider interface { + Execute(ctx context.Context, prompt string) (string, error) +} + +// 모의 구현 생성 +type mockProvider struct { + response string + err error +} + +func (m *mockProvider) Execute(ctx context.Context, prompt string) (string, error) { + return m.response, m.err +} + +// 테스트에서 사용 +func TestConverter_ConvertRule(t *testing.T) { + mock := &mockProvider{ + response: `{"rule": "value"}`, + err: nil, + } + + converter := NewConverter(mock) + result, err := converter.ConvertRule(context.Background(), rule) + + require.NoError(t, err) + assert.Equal(t, expected, result) +} +``` + +### 커버리지 + +`make test` 실행 후 커버리지 데이터가 `coverage.out`에 저장됩니다. + +```bash +# 터미널에서 커버리지 리포트 보기 +go tool cover -func=coverage.out + +# HTML 커버리지 리포트 생성 +go tool cover -html=coverage.out -o coverage.html + +# 브라우저에서 열기 (macOS) +open coverage.html +``` + +--- + +## 참고 문서 + +| 문서 | 설명 | +|------|------| +| [docs/COMMAND.md](COMMAND.md) | 예제가 포함된 완전한 CLI 명령 참조 | +| [internal/llm/README.md](../internal/llm/README.md) | 상세 LLM 프로바이더 구현 가이드 | +| [internal/linter/README.md](../internal/linter/README.md) | 상세 린터 통합 가이드 | From d59a221d685a87d417671b332a401d8d99dc4b56 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:45:38 +0000 Subject: [PATCH 04/13] docs: update project README --- README.md | 148 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 85 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index bae0306..d2104ca 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,59 @@ -# Symphony (sym) +# Symphony -자연어 기반 코딩 컨벤션 관리 및 검증 도구 +**LLM-Friendly Convention Linter for AI Coding Tools** -## 빠른 시작 +Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 코드 컨벤션 검사기입니다. +간단한 설정만으로 프로젝트 규칙을 일관되게 적용하고, LLM 코드 생성 품질을 극대화할 수 있습니다. -### 1. 설치 +--- -```bash -npm i -g @dev-symphony/sym -``` +## 목차 -### 2. 초기화 +- [Symphony](#symphony) + - [목차](#목차) + - [주요 기능](#주요-기능) + - [빠른 시작](#빠른-시작) + - [MCP 설정](#mcp-설정) + - [사용 가능한 MCP 도구](#사용-가능한-mcp-도구) + - [`query_conventions`](#query_conventions) + - [`validate_code`](#validate_code) + - [컨벤션 파일](#컨벤션-파일) + - [요구사항](#요구사항) + - [지원 플랫폼](#지원-플랫폼) + - [라이선스](#라이선스) -```bash -# 프로젝트 초기화 (.sym/ 폴더 생성, MCP 자동 설정) -sym init -``` +--- + +## 주요 기능 -### 3. 사용 +- 자연어로 컨벤션 정의 +- LLM이 MCP를 통해 필요한 컨벤션만 추출하여 컨텍스트에 포함 +- LLM이 MCP를 통해 코드 변경사항에 대한 컨벤션 준수 여부를 검사 +- RBAC 기반 접근 제어 + +--- + +## 빠른 시작 -**웹 대시보드로 컨벤션 편집:** ```bash +# 1. CLI 설치 +npm install -g @dev-symphony/sym + +# 2. 프로젝트 초기화 (.sym/ 폴더 생성 + MCP 설정) +sym init + +# 3. 대시보드 실행 및 컨벤션 편집 sym dashboard -# http://localhost:8787 에서 역할/컨벤션 편집 -``` -## 주요 기능 +# 4. MCP 서버를 LLM IDE 내부에서 사용 +``` -- 자연어로 코딩 컨벤션 정의 (`.sym/user-policy.json`) -- RBAC 기반 파일 접근 권한 관리 -- LLM으로 자연어 규칙을 ESLint/Checkstyle/PMD 설정으로 변환 -- MCP 서버로 Claude, Cursor 등 AI 도구와 연동 -- 웹 대시보드에서 시각적으로 정책 편집 +--- ## MCP 설정 -`sym init` 실행 시 MCP가 자동으로 설정됩니다. - -수동 설정이 필요한 경우: +`sym init` 명령은 MCP 서버 구성을 자동으로 설정합니다. +만약 수동으로 설정하고 싶다면 아래를 `~/.config/.../config.json` 등에 추가하세요. ```json { @@ -50,61 +66,67 @@ sym dashboard } ``` -## CLI 명령어 +--- + +## 사용 가능한 MCP 도구 -| 명령어 | 설명 | -|--------|------| -| `sym init` | 프로젝트 초기화 (.sym/ 생성) | -| `sym dashboard` | 웹 대시보드 실행 (포트 8787) | -| `sym my-role` | 내 역할 확인 | -| `sym policy validate` | 정책 파일 유효성 검사 | -| `sym convert -i user-policy.json --targets all` | 컨벤션을 linter 설정으로 변환 | -| `sym mcp` | MCP 서버 실행 | +### `query_conventions` -## 정책 파일 예시 +- 프로젝트 컨벤션을 조회합니다. +- 카테고리, 파일 목록, 언어 등의 파라미터는 모두 optional입니다. -`.sym/user-policy.json`: +### `validate_code` + +- 코드가 정의된 규칙을 따르는지 검사합니다. +- 필수 파라미터: `files` + +--- + +## 컨벤션 파일 + +Symphony는 프로젝트 컨벤션을 **정책 파일(`.sym/user-policy.json`)**로 관리합니다. +아래 명령으로 대시보드를 열어 쉽게 편집할 수 있습니다. + +```bash +sym dashboard +``` + +예시 정책 파일: ```json { "version": "1.0.0", - "rbac": { - "roles": { - "admin": { "allowWrite": ["**/*"] }, - "developer": { "allowWrite": ["src/**"], "denyWrite": [".sym/**"] } - } - }, "rules": [ - { "say": "클래스 이름은 PascalCase", "category": "naming" }, - { "say": "한 줄은 100자 이하", "category": "formatting" } + { + "say": "Functions should be documented", + "category": "documentation" + }, + { + "say": "Lines should be less than 100 characters", + "category": "formatting", + "params": { "max": 100 } + } ] } ``` -`.sym/roles.json`: +--- -```json -{ - "admin": ["alice"], - "developer": ["bob", "charlie"] -} -``` +## 요구사항 -## 개발 +- Node.js >= 16.0.0 +- Policy file: `.sym/user-policy.json` -```bash -make setup # 의존성 설치 -make build # 빌드 -make test # 테스트 -make lint # 린트 -``` +--- -**필수 도구:** Go 1.21+, Node.js 18+ +## 지원 플랫폼 -## 라이선스 +- macOS (Intel, Apple Silicon) +- Linux (x64, ARM64) +- Windows (x64) -MIT License +--- -## 지원 +## 라이선스 -- GitHub Issues: https://github.com/DevSymphony/sym-cli/issues +MIT From 3e66e3570337376cea577f50b30ee05e6a623dbc Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:45:48 +0000 Subject: [PATCH 05/13] docs: add internal package documentation --- internal/cmd/README.md | 184 ++++++++++++++++++++++++++++++++++- internal/converter/README.md | 63 ++++++++++-- internal/linter/README.md | 139 +++++++++++++++----------- internal/llm/README.md | 113 +++++++++++---------- internal/mcp/README.md | 96 +++++++++++++++++- internal/policy/README.md | 87 ++++++++++++++++- internal/roles/README.md | 107 ++++++++++---------- internal/server/README.md | 93 +++++++++++++++++- internal/validator/README.md | 139 +++++++++++++------------- 9 files changed, 768 insertions(+), 253 deletions(-) diff --git a/internal/cmd/README.md b/internal/cmd/README.md index 6b72757..256ab9d 100644 --- a/internal/cmd/README.md +++ b/internal/cmd/README.md @@ -1,8 +1,184 @@ # cmd -sym CLI 커맨드를 구현합니다. +Symphony CLI 명령어를 구현합니다. -Cobra 기반으로 validate, convert, policy, login, dashboard 등의 명령어를 제공합니다. +Cobra 프레임워크 기반으로 init, validate, convert, policy, dashboard, my-role, llm, mcp, version 등의 명령어를 제공합니다. -**사용자**: 없음 (진입점) -**의존성**: auth, config, converter, git, github, llm, mcp, policy, roles, server, validator +## 패키지 구조 + +``` +cmd/ +├── root.go # 루트 명령어, Execute(), 전역 플래그 +├── version.go # sym version 명령어 +├── colors.go # 터미널 포맷팅 유틸리티 (ANSI 색상) +├── init.go # sym init 명령어 (프로젝트 초기화) +├── dashboard.go # sym dashboard 명령어 (웹 대시보드) +├── my_role.go # sym my-role 명령어 (역할 관리) +├── policy.go # sym policy path|validate 명령어 (정책 관리) +├── validate.go # sym validate 명령어 (코드 검증) +├── convert.go # sym convert 명령어 (정책 변환) +├── llm.go # sym llm status|test|setup 명령어 (LLM 관리) +├── mcp.go # sym mcp 명령어 (MCP 서버) +├── mcp_register.go # MCP 서버 등록 헬퍼 함수 +├── survey_templates.go # 커스텀 survey UI 템플릿 +└── README.md +``` + +## 의존성 + +### 패키지 사용자 + +| 위치 | 용도 | +|------|------| +| `cmd/sym/main.go` | CLI 진입점, `Execute()` 및 `SetVersion()` 호출 | + +### 패키지 의존성 + +``` + ┌───────────┐ + │ cmd │ + └─────┬─────┘ + ┌──────┬──────┬───────┼───────┬──────┬──────┬──────┐ + ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ + converter llm validator policy roles server mcp linter + │ │ │ │ │ │ │ │ + └──────┴──────┴───────┴───────┴──────┴──────┴──────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + util/config util/git util/env + │ + ▼ + pkg/schema +``` + +## Public / Private API + +### Public API + +#### Functions + +| 함수 | 파일 | 설명 | +|------|------|------| +| `Execute()` | root.go:26 | CLI 실행 진입점 | +| `SetVersion(v string)` | version.go:26 | 버전 문자열 설정 (main.go에서 호출) | + +### Private API + +#### 명령어 변수 (cobra.Command) + +| 변수 | 파일 | 설명 | +|------|------|------| +| `rootCmd` | root.go:13 | 루트 명령어 (sym) | +| `versionCmd` | version.go:12 | version 명령어 | +| `initCmd` | init.go:18 | init 명령어 | +| `dashboardCmd` | dashboard.go:13 | dashboard 명령어 | +| `myRoleCmd` | my_role.go:14 | my-role 명령어 | +| `policyCmd` | policy.go:13 | policy 명령어 | +| `policyPathCmd` | policy.go:24 | policy path 명령어 | +| `policyValidateCmd` | policy.go:31 | policy validate 명령어 | +| `validateCmd` | validate.go:23 | validate 명령어 | +| `convertCmd` | convert.go:22 | convert 명령어 | +| `llmCmd` | llm.go:18 | llm 명령어 | +| `llmStatusCmd` | llm.go:33 | llm status 명령어 | +| `llmTestCmd` | llm.go:40 | llm test 명령어 | +| `llmSetupCmd` | llm.go:47 | llm setup 명령어 | +| `mcpCmd` | mcp.go:15 | mcp 명령어 | + +#### 명령어 실행 함수 + +| 함수 | 파일 | 설명 | +|------|------|------| +| `runInit(cmd, args)` | init.go:47 | init 명령어 실행 | +| `runDashboard(cmd, args)` | dashboard.go:32 | dashboard 실행 | +| `runMyRole(cmd, args)` | my_role.go:34 | my-role 실행 | +| `runPolicyPath(cmd, args)` | policy.go:49 | policy path 실행 | +| `runPolicyValidate(cmd, args)` | policy.go:92 | policy validate 실행 | +| `runValidate(cmd, args)` | validate.go:55 | validate 실행 | +| `runConvert(cmd, args)` | convert.go:44 | convert 실행 | +| `runLLMStatus(cmd, args)` | llm.go:61 | llm status 실행 | +| `runLLMTest(cmd, args)` | llm.go:107 | llm test 실행 | +| `runLLMSetup(cmd, args)` | llm.go:142 | llm setup 실행 | +| `runMCP(cmd, args)` | mcp.go:37 | mcp 실행 | + +#### 헬퍼 함수 - 초기화 + +| 함수 | 파일 | 설명 | +|------|------|------| +| `createDefaultPolicy()` | init.go:135 | 기본 정책 파일 생성 | +| `initializeConfigFile()` | init.go:186 | config.json 초기화 | +| `removeExistingCodePolicy()` | init.go:201 | 생성된 파일 정리 (--force) | + +#### 헬퍼 함수 - LLM + +| 함수 | 파일 | 설명 | +|------|------|------| +| `promptLLMBackendSetup()` | llm.go:198 | LLM 프로바이더 설정 프롬프트 | +| `promptAndSaveAPIKey(provider)` | llm.go:276 | API 키 입력 및 저장 | +| `ensureGitignore(path)` | llm.go:318 | .gitignore 업데이트 | + +#### 헬퍼 함수 - MCP 등록 + +| 함수 | 파일 | 설명 | +|------|------|------| +| `promptMCPRegistration()` | mcp_register.go:64 | MCP 등록 프롬프트 | +| `registerMCP(app)` | mcp_register.go:135 | MCP 서버 등록 | +| `getMCPConfigPath(app)` | mcp_register.go:257 | MCP 설정 파일 경로 | +| `getAppDisplayName(app)` | mcp_register.go:274 | 앱 표시명 반환 | +| `checkNpxAvailable()` | mcp_register.go:288 | npx 설치 여부 확인 | +| `updateSymphonySection(content)` | mcp_register.go:294 | Symphony 섹션 업데이트 | +| `createInstructionsFile(app)` | mcp_register.go:325 | 지시 파일 생성 | +| `getClaudeCodeInstructions()` | mcp_register.go:395 | Claude Code 지시 반환 | +| `getCursorInstructions()` | mcp_register.go:429 | Cursor 지시 반환 | +| `getVSCodeInstructions()` | mcp_register.go:467 | VS Code 지시 반환 | + +#### 헬퍼 함수 - 역할 + +| 함수 | 파일 | 설명 | +|------|------|------| +| `selectNewRole()` | my_role.go:81 | 새 역할 선택 | + +#### 헬퍼 함수 - 출력 포맷 + +| 함수 | 파일 | 설명 | +|------|------|------| +| `printValidationResult(result)` | validate.go:131 | 검증 결과 출력 | +| `runNewConverter(policy)` | convert.go:74 | 새 컨버터 실행 | + +#### 터미널 포맷팅 (colors.go) + +| 함수 | 파일 | 설명 | +|------|------|------| +| `isTTY()` | colors.go:21 | TTY 여부 확인 | +| `colorize(color, msg)` | colors.go:26 | 색상 적용 | +| `ok(msg)` | colors.go:34 | [OK] 접두어 포맷 | +| `formatError(msg)` | colors.go:40 | [ERROR] 접두어 포맷 | +| `warn(msg)` | colors.go:46 | [WARN] 접두어 포맷 | +| `titleWithDesc(title, desc)` | colors.go:52 | 섹션 제목 포맷 | +| `done(msg)` | colors.go:58 | [DONE] 접두어 포맷 | +| `printOK(msg)` | colors.go:64 | OK 메시지 출력 | +| `printError(msg)` | colors.go:69 | 에러 메시지 출력 | +| `printWarn(msg)` | colors.go:74 | 경고 메시지 출력 | +| `printTitle(title, desc)` | colors.go:79 | 제목 출력 | +| `printDone(msg)` | colors.go:84 | 완료 메시지 출력 | +| `indent(msg)` | colors.go:89 | 들여쓰기 | + +#### Survey 템플릿 (survey_templates.go) + +| 함수 | 파일 | 설명 | +|------|------|------| +| `useSelectTemplateNoFilter()` | survey_templates.go:53 | 필터 없는 Select 템플릿 적용 | +| `useMultiSelectTemplateNoFilter()` | survey_templates.go:64 | 필터 없는 MultiSelect 템플릿 적용 | + +#### Types + +| 타입 | 파일 | 설명 | +|------|------|------| +| `MCPRegistrationConfig` | mcp_register.go:22 | MCP 설정 구조 (mcpServers 포맷) | +| `VSCodeMCPConfig` | mcp_register.go:27 | VS Code MCP 설정 구조 | +| `MCPServerConfig` | mcp_register.go:34 | MCP 서버 설정 | +| `VSCodeServerConfig` | mcp_register.go:42 | VS Code 서버 설정 | + +## 참고 문헌 + +- [명령어 레퍼런스](/docs/COMMAND.md) - 상세 CLI 사용법, 플래그, 예시 diff --git a/internal/converter/README.md b/internal/converter/README.md index 4e04926..05303b8 100644 --- a/internal/converter/README.md +++ b/internal/converter/README.md @@ -1,13 +1,62 @@ # converter -UserPolicy(A Schema)를 CodePolicy(B Schema)로 변환합니다. +UserPolicy(A Schema)를 CodePolicy(B Schema)로 변환하는 핵심 모듈입니다. -자연어 규칙을 구조화된 검증 규칙으로 변환하고 ESLint, Prettier, Checkstyle, PMD 등의 linter 설정 파일을 자동 생성합니다. +LLM을 활용하여 자연어 규칙을 적절한 린터로 라우팅하고, 각 린터의 설정 파일과 `code-policy.json`을 자동 생성합니다. -## 서브패키지 +## 패키지 구조 -- `linters`: 각 linter별 설정 파일 생성기 (ESLint, Prettier, Checkstyle, PMD) -- `linters/registry`: Linter 변환기 등록 및 검색 시스템 +``` +internal/converter/ +├── converter.go # 변환 로직 전체 +└── README.md # 이 문서 +``` -**사용자**: cmd, mcp -**의존성**: llm, schema +## 의존성 + +### 패키지 사용자 + +| 패키지 | 용도 | +|--------|------| +| `internal/cmd` | CLI `convert` 명령어 | +| `internal/mcp` | MCP 서버의 policy 변환 | +| `internal/server` | 웹 대시보드의 변환 기능 | + +### 패키지 의존성 + +| 패키지 | 용도 | +|--------|------| +| `internal/linter` | 린터 레지스트리 및 Converter 인터페이스 | +| `internal/llm` | LLM Provider 인터페이스 | +| `pkg/schema` | UserPolicy, CodePolicy 스키마 정의 | + +## Public/Private API + +### Public API + +#### Types + +| 타입 | 설명 | +|------|------| +| `Converter` | 변환기 인스턴스. LLM Provider와 출력 디렉토리를 포함 | +| `ConvertResult` | 변환 결과. 생성된 파일 목록, CodePolicy, 오류, 경고 포함 | + +#### Functions + +| 함수 | 시그니처 | 설명 | +|------|----------|------| +| `NewConverter` | `(provider llm.Provider, outputDir string) *Converter` | 새 Converter 인스턴스 생성 | +| `Convert` | `(ctx context.Context, userPolicy *schema.UserPolicy) (*ConvertResult, error)` | UserPolicy를 CodePolicy와 린터 설정으로 변환 | + +### Private API + +| 함수 | 설명 | +|------|------| +| `routeRulesWithLLM` | LLM을 사용하여 규칙을 적합한 린터로 라우팅 | +| `getAvailableLinters` | 지정된 언어에 대해 사용 가능한 린터 목록 조회 | +| `selectLintersForRule` | 개별 규칙에 적합한 린터 선택 (LLM 활용) | +| `getLinterConverter` | 레지스트리에서 린터 변환기 조회 | +| `buildLinterDescriptions` | LLM 프롬프트용 린터 설명 문자열 생성 | +| `buildRoutingHints` | LLM 프롬프트용 라우팅 힌트 문자열 생성 | +| `convertAllTasks` | 세마포어 기반 병렬 변환 실행 | +| `convertRBAC` | UserRBAC를 PolicyRBAC로 변환 | diff --git a/internal/linter/README.md b/internal/linter/README.md index 607c893..3e6e545 100644 --- a/internal/linter/README.md +++ b/internal/linter/README.md @@ -1,53 +1,61 @@ -# Linter Package +# Linter 패키지 -Unified interface for static linting tools. +정적 린팅 도구를 위한 통합 인터페이스. -## File Structure +## 파일 구조 ``` internal/linter/ -├── linter.go # Linter interface (execution) -├── converter.go # Converter interface (rule conversion) -├── registry.go # Global registry, RegisterTool(), GetLinter() +├── linter.go # Linter 인터페이스 (실행) +├── converter.go # Converter 인터페이스 (규칙 변환) +├── registry.go # 전역 레지스트리, RegisterTool(), GetLinter() ├── helpers.go # CleanJSONResponse, DefaultToolsDir, WriteTempConfig ├── subprocess.go # SubprocessExecutor ├── eslint/ # JavaScript/TypeScript -├── prettier/ # Code formatting +├── prettier/ # 코드 포맷팅 ├── pylint/ # Python -├── tsc/ # TypeScript type checking -├── checkstyle/ # Java style -└── pmd/ # Java static analysis +├── tsc/ # TypeScript 타입 검사 +├── checkstyle/ # Java 스타일 +└── pmd/ # Java 정적 분석 + +각 린터 서브디렉토리 구성: +├── linter.go # Linter 구현 +├── converter.go # Converter 구현 +├── executor.go # 도구 실행 로직 +├── parser.go # 출력 파싱 +├── register.go # init() 등록 +└── *_test.go # 단위 테스트 (eslint, prettier, pylint, tsc) ``` -## Usage +## 사용법 ```go import "github.com/DevSymphony/sym-cli/internal/linter" -// 1. Get linter by name +// 1. 이름으로 린터 가져오기 l, err := linter.Global().GetLinter("eslint") if err != nil { return err } -// 2. Check availability and install if needed +// 2. 가용성 확인 및 필요시 설치 if err := l.CheckAvailability(ctx); err != nil { if err := l.Install(ctx, linter.InstallConfig{}); err != nil { return err } } -// 3. Execute linter +// 3. 린터 실행 output, err := l.Execute(ctx, config, files) if err != nil { return err } -// 4. Parse output to violations +// 4. 출력을 위반 사항으로 파싱 violations, err := l.ParseOutput(output) ``` -### Getting Converter +### Converter 가져오기 ```go converter, ok := linter.Global().GetConverter("eslint") @@ -55,17 +63,34 @@ if !ok { return fmt.Errorf("converter not found") } -// Convert single rule (called by main converter in parallel) +// 단일 규칙 변환 (메인 컨버터가 병렬로 호출) result, err := converter.ConvertSingleRule(ctx, rule, llmProvider) -// Build config from results +// 결과로 설정 빌드 config, err := converter.BuildConfig(results) ``` -## Linter List +### Registry 메서드 -| Name | Languages | Config File | -|------|-----------|-------------| +```go +// 핵심 메서드 +linter.Global() // 싱글톤 Registry 인스턴스 반환 +linter.Global().RegisterTool(l, c, cfg) // 린터, 컨버터, 설정 파일 등록 +linter.Global().GetLinter("eslint") // 이름으로 Linter 가져오기 (없으면 에러 반환) +linter.Global().GetConverter("eslint") // 이름으로 Converter 가져오기 (bool 반환) +linter.Global().GetConfigFile("eslint") // 설정 파일명 가져오기 (예: ".eslintrc.json") + +// 일괄 조회 메서드 +linter.Global().GetAllToolNames() // []string - 등록된 모든 도구 이름 +linter.Global().GetAllConfigFiles() // []string - 모든 설정 파일명 +linter.Global().GetAllConverters() // []Converter - 등록된 모든 컨버터 +linter.Global().BuildLanguageMapping() // map[string][]string - 언어별 도구 매핑 +``` + +## 린터 목록 + +| 이름 | 언어 | 설정 파일 | +|------|------|----------| | `eslint` | JavaScript, TypeScript, JSX, TSX | `.eslintrc.json` | | `prettier` | JS, TS, JSON, CSS, HTML, Markdown | `.prettierrc` | | `pylint` | Python | `.pylintrc` | @@ -73,20 +98,20 @@ config, err := converter.BuildConfig(results) | `checkstyle` | Java | `checkstyle.xml` | | `pmd` | Java | `pmd.xml` | -## Adding New Linter +## 새 린터 추가 -### Step 1: Create Directory +### 1단계: 디렉토리 생성 ``` internal/linter// -├── linter.go # Linter implementation -├── converter.go # Converter implementation -├── executor.go # Tool execution logic -├── parser.go # Output parsing -└── register.go # init() registration +├── linter.go # Linter 구현 +├── converter.go # Converter 구현 +├── executor.go # 도구 실행 로직 +├── parser.go # 출력 파싱 +└── register.go # init() 등록 ``` -### Step 2: Implement Linter Interface +### 2단계: Linter 인터페이스 구현 ```go package mylinter @@ -96,7 +121,7 @@ import ( "github.com/DevSymphony/sym-cli/internal/linter" ) -// Compile-time check +// 컴파일 타임 검사 var _ linter.Linter = (*Linter)(nil) type Linter struct { @@ -126,23 +151,23 @@ func (l *Linter) GetCapabilities() linter.Capabilities { } func (l *Linter) CheckAvailability(ctx context.Context) error { - // Check if tool binary exists + // 도구 바이너리 존재 여부 확인 } func (l *Linter) Install(ctx context.Context, cfg linter.InstallConfig) error { - // Install tool (gem install, pip install, npm install, etc.) + // 도구 설치 (gem install, pip install, npm install 등) } func (l *Linter) Execute(ctx context.Context, config []byte, files []string) (*linter.ToolOutput, error) { - // Run tool and return output + // 도구 실행 및 출력 반환 } func (l *Linter) ParseOutput(output *linter.ToolOutput) ([]linter.Violation, error) { - // Parse tool-specific output to violations + // 도구별 출력을 위반 사항으로 파싱 } ``` -### Step 3: Implement Converter Interface +### 3단계: Converter 인터페이스 구현 ```go package mylinter @@ -154,7 +179,7 @@ import ( "github.com/DevSymphony/sym-cli/pkg/schema" ) -// Compile-time check +// 컴파일 타임 검사 var _ linter.Converter = (*Converter)(nil) type Converter struct{} @@ -168,45 +193,45 @@ func (c *Converter) SupportedLanguages() []string { } func (c *Converter) GetLLMDescription() string { - return `Ruby code quality (style, naming, complexity) - - CAN: Naming conventions, line length, cyclomatic complexity - - CANNOT: Business logic, runtime behavior` + return `Ruby 코드 품질 (스타일, 네이밍, 복잡도) + - 가능: 네이밍 규칙, 줄 길이, 순환 복잡도 + - 불가: 비즈니스 로직, 런타임 동작` } func (c *Converter) GetRoutingHints() []string { return []string{ - "For Ruby code style → use mylinter", - "For Ruby naming conventions → use mylinter", + "Ruby 코드 스타일 → mylinter 사용", + "Ruby 네이밍 규칙 → mylinter 사용", } } -// ConvertSingleRule converts ONE user rule to linter-specific data. -// Concurrency is handled by main converter, not here. +// ConvertSingleRule은 하나의 사용자 규칙을 린터별 데이터로 변환합니다. +// 동시성은 메인 컨버터가 관리하며, 여기서는 처리하지 않습니다. func (c *Converter) ConvertSingleRule(ctx context.Context, rule schema.UserRule, provider llm.Provider) (*linter.SingleRuleResult, error) { - // Call LLM to convert rule + // LLM을 호출하여 규칙 변환 config, err := c.callLLM(ctx, rule, provider) if err != nil { return nil, err } - // Return nil, nil if rule cannot be enforced by this linter + // 이 린터로 규칙을 적용할 수 없으면 nil, nil 반환 if config == nil { return nil, nil } return &linter.SingleRuleResult{ RuleID: rule.ID, - Data: config, // Linter-specific data + Data: config, // 린터별 데이터 }, nil } -// BuildConfig assembles final config from all successful conversions. +// BuildConfig는 모든 성공적인 변환 결과로 최종 설정을 조립합니다. func (c *Converter) BuildConfig(results []*linter.SingleRuleResult) (*linter.LinterConfig, error) { if len(results) == 0 { return nil, nil } - // Build config from results + // 결과로 설정 빌드 config := buildMyLinterConfig(results) content, err := json.MarshalIndent(config, "", " ") @@ -222,7 +247,7 @@ func (c *Converter) BuildConfig(results []*linter.SingleRuleResult) (*linter.Lin } ``` -### Step 4: Register in init() +### 4단계: init()에서 등록 ```go package mylinter @@ -238,7 +263,7 @@ func init() { } ``` -### Step 5: Add Import to Bootstrap +### 5단계: Bootstrap에 임포트 추가 ```go // cmd/sym/bootstrap.go @@ -247,15 +272,15 @@ import ( ) ``` -## Key Rules +## 주요 규칙 -- Add compile-time checks for both interfaces: +- 두 인터페이스 모두에 컴파일 타임 검사 추가: ```go var _ linter.Linter = (*Linter)(nil) var _ linter.Converter = (*Converter)(nil) ``` -- `ConvertSingleRule()` handles ONE rule only - concurrency is managed by main converter -- Return `(nil, nil)` from `ConvertSingleRule()` if rule cannot be enforced (falls back to llm-validator) -- Use `linter.CleanJSONResponse()` to strip markdown fences from LLM responses -- Return clear error messages if tool not installed -- Refer to existing linters (eslint, pylint) for patterns +- `ConvertSingleRule()`은 하나의 규칙만 처리 - 동시성은 메인 컨버터가 관리 +- 규칙을 적용할 수 없으면 `ConvertSingleRule()`에서 `(nil, nil)` 반환 (llm-validator로 폴백) +- LLM 응답에서 마크다운 펜스 제거에 `linter.CleanJSONResponse()` 사용 +- 도구가 설치되지 않은 경우 명확한 오류 메시지 반환 +- 패턴 참고를 위해 기존 린터 (eslint, pylint) 참조 diff --git a/internal/llm/README.md b/internal/llm/README.md index b865732..740ff97 100644 --- a/internal/llm/README.md +++ b/internal/llm/README.md @@ -1,43 +1,42 @@ -# LLM Package +# LLM 패키지 -Unified interface for LLM providers. +LLM 프로바이더를 위한 통합 인터페이스입니다. -## File Structure +## 파일 구조 ``` internal/llm/ -├── llm.go # Provider, RawProvider interface, Config, ResponseFormat -├── registry.go # RegisterProvider, New, ListProviders -├── wrapper.go # parsedProvider (auto parsing wrapper) -├── config.go # LoadConfig -├── parser.go # Response parsing (private) -├── DESIGN.md # Architecture documentation -├── claudecode/ # Claude Code CLI provider -├── geminicli/ # Gemini CLI provider -└── openaiapi/ # OpenAI API provider +├── llm.go # Provider, RawProvider 인터페이스, Config, ResponseFormat, ModelInfo, APIKeyConfig, ProviderInfo +├── registry.go # 프로바이더 레지스트리 및 유틸리티 함수 +├── wrapper.go # parsedProvider (자동 파싱 래퍼) +├── config.go # LoadConfig, LoadConfigFromDir, Config.Validate +├── parser.go # 응답 파싱 (비공개) +├── claudecode/ # Claude Code CLI 프로바이더 +├── geminicli/ # Gemini CLI 프로바이더 +└── openaiapi/ # OpenAI API 프로바이더 ``` -## Usage +## 사용법 ```go import "github.com/DevSymphony/sym-cli/internal/llm" -// 1. Load config +// 1. 설정 로드 cfg := llm.LoadConfig() -// 2. Create provider +// 2. 프로바이더 생성 provider, err := llm.New(cfg) if err != nil { - return err // CLI not installed or API key missing + return err // CLI 미설치 또는 API 키 누락 } -// 3. Execute prompt +// 3. 프롬프트 실행 response, err := provider.Execute(ctx, prompt, llm.JSON) ``` -### Configuration +### 설정 -Config file: `.sym/config.json` +설정 파일: `.sym/config.json` ```json { @@ -48,31 +47,31 @@ Config file: `.sym/config.json` } ``` -For OpenAI API, also add API key to `.sym/.env`: +OpenAI API 사용 시 `.sym/.env`에 API 키 추가 필요: ```bash OPENAI_API_KEY=sk-... ``` -### Response Format +### 응답 형식 -| Format | Description | -|--------|-------------| -| `llm.Text` | Return raw text | -| `llm.JSON` | Extract JSON from LLM response | -| `llm.XML` | Extract XML from LLM response | +| 형식 | 설명 | +|------|------| +| `llm.Text` | 원시 텍스트 반환 | +| `llm.JSON` | LLM 응답에서 JSON 추출 | +| `llm.XML` | LLM 응답에서 XML 추출 | -`llm.JSON` and `llm.XML` automatically extract structured data when LLM returns JSON/XML with preamble text. +`llm.JSON`과 `llm.XML`은 LLM이 서두 텍스트와 함께 JSON/XML을 반환할 때 자동으로 구조화된 데이터를 추출합니다. -## Provider List +## 프로바이더 목록 -| Name | Type | Default Model | Installation | -|------|------|---------------|--------------| +| 이름 | 유형 | 기본 모델 | 설치 방법 | +|------|------|-----------|-----------| | `claudecode` | CLI | sonnet | `npm i -g @anthropic-ai/claude-cli` | | `geminicli` | CLI | gemini-2.5-flash | `npm i -g @google/gemini-cli` | -| `openaiapi` | API | gpt-4o-mini | Set `OPENAI_API_KEY` env var | +| `openaiapi` | API | gpt-4o-mini | `OPENAI_API_KEY` 환경 변수 설정 | -### Check Provider Status +### 프로바이더 상태 확인 ```go providers := llm.ListProviders() @@ -81,16 +80,16 @@ for _, p := range providers { } ``` -## Adding New Provider +## 새 프로바이더 추가 -### Step 1: Create Directory +### 1단계: 디렉토리 생성 ``` internal/llm// └── provider.go ``` -### Step 2: Implement RawProvider Interface +### 2단계: RawProvider 인터페이스 구현 ```go package myprovider @@ -104,7 +103,7 @@ type Provider struct { model string } -// Compile-time check: Provider must implement RawProvider interface +// 컴파일 타임 검사: Provider가 RawProvider 인터페이스를 구현하는지 확인 var _ llm.RawProvider = (*Provider)(nil) func (p *Provider) Name() string { @@ -112,9 +111,9 @@ func (p *Provider) Name() string { } func (p *Provider) ExecuteRaw(ctx context.Context, prompt string, format llm.ResponseFormat) (string, error) { - // Execute prompt and return raw response + // 프롬프트 실행 후 원시 응답 반환 response := callLLM(prompt) - return response, nil // No manual parsing needed + return response, nil // 수동 파싱 불필요 } func (p *Provider) Close() error { @@ -122,9 +121,9 @@ func (p *Provider) Close() error { } ``` -> **Note**: The `var _ llm.RawProvider = (*Provider)(nil)` line is a compile-time check that ensures `Provider` implements the `RawProvider` interface. If any method is missing or has the wrong signature, the code will fail to compile. +> **참고**: `var _ llm.RawProvider = (*Provider)(nil)` 라인은 `Provider`가 `RawProvider` 인터페이스를 구현하는지 확인하는 컴파일 타임 검사입니다. 메서드가 누락되었거나 시그니처가 잘못된 경우 컴파일에 실패합니다. -### Step 3: Register in init() +### 3단계: init()에서 등록 ```go func init() { @@ -132,22 +131,22 @@ func init() { Name: "myprovider", DisplayName: "My Provider", DefaultModel: "default-model-v1", - Available: checkAvailability(), // Check CLI exists or API key set - Path: cliPath, // CLI path (empty for API) + Available: checkAvailability(), // CLI 존재 또는 API 키 설정 여부 확인 + Path: cliPath, // CLI 경로 (API의 경우 빈 문자열) Models: []llm.ModelInfo{ - {ID: "model-v1", DisplayName: "Model V1", Description: "Standard model", Recommended: true}, - {ID: "model-v2", DisplayName: "Model V2", Description: "Advanced model", Recommended: false}, + {ID: "model-v1", DisplayName: "Model V1", Description: "표준 모델", Recommended: true}, + {ID: "model-v2", DisplayName: "Model V2", Description: "고급 모델", Recommended: false}, }, APIKey: llm.APIKeyConfig{ - Required: true, // Set false for CLI-based providers - EnvVarName: "MY_PROVIDER_API_KEY", // Environment variable name - Prefix: "mp-", // Expected prefix for validation (optional) + Required: true, // CLI 기반 프로바이더는 false로 설정 + EnvVarName: "MY_PROVIDER_API_KEY", // 환경 변수 이름 + Prefix: "mp-", // 검증용 예상 접두사 (선택사항) }, }) } func newProvider(cfg llm.Config) (llm.RawProvider, error) { - // Return error if CLI not installed or API key missing + // CLI 미설치 또는 API 키 누락 시 에러 반환 if !isAvailable() { return nil, fmt.Errorf("provider not available") } @@ -161,7 +160,7 @@ func newProvider(cfg llm.Config) (llm.RawProvider, error) { } ``` -### Step 4: Add Import to Bootstrap +### 4단계: 부트스트랩에 import 추가 ```go // cmd/sym/bootstrap.go @@ -170,12 +169,12 @@ import ( ) ``` -### Key Rules +### 핵심 규칙 -- Add compile-time check: `var _ llm.RawProvider = (*Provider)(nil)` -- Implement `RawProvider.ExecuteRaw()` - parsing is handled automatically by wrapper -- Return clear error message if CLI not installed or API key missing -- Check availability in init() to set ProviderInfo.Available -- Define `Models` with at least one model marked as `Recommended: true` -- Set `APIKey.Required: false` for CLI-based providers (they handle auth internally) -- Refer to existing providers (claudecode, openaiapi) for patterns +- 컴파일 타임 검사 추가: `var _ llm.RawProvider = (*Provider)(nil)` +- `RawProvider.ExecuteRaw()` 구현 - 파싱은 래퍼에서 자동 처리 +- CLI 미설치 또는 API 키 누락 시 명확한 에러 메시지 반환 +- init()에서 가용성을 확인하여 ProviderInfo.Available 설정 +- `Models`에 최소 하나의 모델을 `Recommended: true`로 정의 +- CLI 기반 프로바이더는 `APIKey.Required: false` 설정 (자체적으로 인증 처리) +- 기존 프로바이더(claudecode, openaiapi) 패턴 참조 diff --git a/internal/mcp/README.md b/internal/mcp/README.md index 0fbc308..c2154a1 100644 --- a/internal/mcp/README.md +++ b/internal/mcp/README.md @@ -1,8 +1,96 @@ # mcp -Model Context Protocol 서버를 구현합니다. +MCP (Model Context Protocol) 서버 구현 -바이브코딩 도구와의 통신 인터페이스를 제공하며 정책 변환 및 검증 기능을 노출합니다. +AI 코딩 도구(Claude Code, Cursor 등)와 stdio를 통해 통신하며, +컨벤션 조회와 코드 검증 기능을 제공합니다. -**사용자**: cmd -**의존성**: converter, git, llm, policy, validator +## 패키지 구조 + +``` +mcp/ +├── server.go # MCP 서버 구현 (NewServer, Start, 도구 핸들러) +├── server_test.go # query_conventions 테스트 +└── README.md +``` + +## 의존성 + +### 패키지 사용자 + +| 위치 | 용도 | +|------|------| +| `internal/cmd/mcp.go` | `sym mcp` CLI 명령어 | + +### 패키지 의존성 + +``` + ┌───────────┐ + │ mcp │ + └─────┬─────┘ + ┌───────┬───────┼───────┬───────┐ + ▼ ▼ ▼ ▼ ▼ +┌─────────┐ ┌─────┐ ┌───────┐ ┌─────┐ ┌─────────┐ +│converter│ │ llm │ │policy │ │roles│ │validator│ +└─────────┘ └─────┘ └───────┘ └─────┘ └─────────┘ + │ + ┌───────┴───────┐ + ▼ ▼ + ┌──────────┐ ┌────────────┐ + │ util/git │ │ pkg/schema │ + └──────────┘ └────────────┘ +``` + +## Public / Private API + +### Public API + +#### Types + +| 타입 | 파일 | 설명 | +|------|------|------| +| `Server` | server.go:78 | MCP 서버 인스턴스 | +| `RPCError` | server.go:184 | JSON-RPC 에러 타입 | +| `QueryConventionsInput` | server.go:190 | query_conventions 입력 스키마 | +| `ValidateCodeInput` | server.go:196 | validate_code 입력 스키마 | +| `QueryConventionsRequest` | server.go:244 | 컨벤션 조회 요청 | +| `ConventionItem` | server.go:250 | 컨벤션 항목 | +| `ValidateCodeRequest` | server.go:411 | 검증 요청 | +| `ViolationItem` | server.go:416 | 위반 항목 | +| `ValidationResultRecord` | server.go:720 | 검증 결과 레코드 | +| `ValidationHistory` | server.go:731 | 검증 이력 | + +#### Functions + +| 함수 | 파일 | 설명 | +|------|------|------| +| `ConvertPolicyWithLLM(userPath, codePath)` | server.go:25 | LLM으로 정책 변환 | +| `NewServer(configPath)` | server.go:86 | 서버 생성 | + +#### Methods + +| 메서드 | 파일 | 설명 | +|--------|------|------| +| `(*Server) Start()` | server.go:95 | MCP 서버 시작 | + +### Private API + +| 함수/메서드 | 설명 | +|-------------|------| +| `runStdioWithSDK(ctx)` | MCP SDK로 stdio 서버 실행 | +| `handleQueryConventions(params)` | 컨벤션 조회 핸들러 | +| `filterConventions(req)` | 컨벤션 필터링 | +| `isRuleRelevant(rule, req)` | 규칙 관련성 확인 | +| `handleValidateCode(ctx, session, params)` | 코드 검증 핸들러 | +| `containsAny(haystack, needles)` | 배열 교집합 확인 | +| `getValidationPolicy()` | 검증용 정책 반환 | +| `needsConversion(codePolicyPath)` | 변환 필요 여부 확인 | +| `extractSourceRuleID(id)` | 원본 규칙 ID 추출 | +| `convertUserPolicy(userPath, codePath)` | 정책 변환 래퍼 | +| `getRBACInfo()` | RBAC 정보 생성 | +| `saveValidationResults(result, violations, hasErrors)` | 검증 결과 저장 | + +## 참고 문헌 + +- [MCP 도구 스키마](../../docs/COMMAND.md#mcp-도구-스키마) - query_conventions, validate_code 입력/출력 스펙 +- [MCP 통합 가이드](../../docs/COMMAND.md#mcp-통합) - 지원 도구 및 등록 방법 diff --git a/internal/policy/README.md b/internal/policy/README.md index d29a715..6fafd74 100644 --- a/internal/policy/README.md +++ b/internal/policy/README.md @@ -1,8 +1,87 @@ # policy -정책 파일을 관리합니다. +정책 파일의 로드, 저장, 검증 및 템플릿 관리를 담당하는 패키지입니다. -UserPolicy 및 CodePolicy 로드, 저장, 변경 이력 추적 기능을 제공하며 다양한 프레임워크 템플릿을 포함합니다. +UserPolicy(A 스키마)와 CodePolicy(B 스키마) 파일을 로드하고, 정책 유효성 검증, 저장, 다양한 프레임워크 템플릿 제공 기능을 수행합니다. -**사용자**: cmd, mcp, roles, server -**의존성**: git, schema +## 패키지 구조 + +``` +internal/policy/ +├── loader.go # 정책 파일 로더 (Loader 구조체) +├── manager.go # 정책 관리 함수 (경로, 로드, 저장, 검증) +├── templates.go # 템플릿 관리 (embed.FS 기반) +├── README.md +└── templates/ # 내장 정책 템플릿 (7개) + ├── demo-template.json # 샘플 자바 정책 + ├── go-template.json # Go 마이크로서비스 + ├── node-template.json # Node.js 백엔드 + ├── python-template.json # Python (PEP 8, Django/Flask) + ├── react-template.json # React 프로젝트 + ├── typescript-template.json # TypeScript 라이브러리 + └── vue-template.json # Vue.js 프로젝트 +``` + +## 의존성 + +### 패키지 사용자 + +| 패키지 | 파일 | 사용 목적 | +|--------|------|-----------| +| cmd | policy.go | 정책 경로 표시, 검증 CLI 명령 | +| cmd | init.go | 프로젝트 초기화 시 기본 정책 생성 | +| roles | rbac.go | RBAC 검증을 위한 정책 로드 | +| server | server.go | 대시보드 REST API (정책 CRUD, 템플릿) | +| mcp | server.go | MCP 서버 정책 로드 및 변환 | + +### 패키지 의존성 + +| 패키지 | 사용 목적 | +|--------|-----------| +| internal/util/git | Git 저장소 루트 경로 획득 (GetRepoRoot) | +| pkg/schema | UserPolicy, CodePolicy 스키마 타입 | + +## Public/Private API + +### Public API + +#### Types + +**Loader** (loader.go:12) +```go +type Loader struct{} +``` +정책 파일 로더. `LoadUserPolicy`, `LoadCodePolicy` 메서드 제공. + +**Template** (templates.go:18) +```go +type Template struct { + Name string `json:"name"` + Description string `json:"description"` + Language string `json:"language"` + Framework string `json:"framework,omitempty"` +} +``` +템플릿 메타데이터. + +#### Functions + +| 함수 | 파일 | 설명 | +|------|------|------| +| `NewLoader(verbose bool) *Loader` | loader.go:16 | 새 로더 인스턴스 생성 | +| `(*Loader).LoadUserPolicy(path) (*UserPolicy, error)` | loader.go:21 | A 스키마 로드 | +| `(*Loader).LoadCodePolicy(path) (*CodePolicy, error)` | loader.go:36 | B 스키마 로드 | +| `GetPolicyPath(customPath) (string, error)` | manager.go:16 | 정책 파일 전체 경로 반환 | +| `LoadPolicy(customPath) (*UserPolicy, error)` | manager.go:31 | 정책 로드 (없으면 빈 정책 반환) | +| `SavePolicy(policy, customPath) error` | manager.go:58 | 정책 저장 (검증 후) | +| `ValidatePolicy(policy) error` | manager.go:84 | 정책 구조 유효성 검증 | +| `PolicyExists(customPath) (bool, error)` | manager.go:133 | 정책 파일 존재 여부 | +| `GetTemplates() ([]Template, error)` | templates.go:26 | 템플릿 목록 반환 | +| `GetTemplate(name) (*UserPolicy, error)` | templates.go:81 | 특정 템플릿 로드 | + +### Private API + +| 변수/함수 | 파일 | 설명 | +|-----------|------|------| +| `defaultPolicyPath` | manager.go:13 | 기본 경로: `.sym/user-policy.json` | +| `templateFiles` | templates.go:15 | embed.FS (내장 템플릿) | diff --git a/internal/roles/README.md b/internal/roles/README.md index e856c5e..483c29b 100644 --- a/internal/roles/README.md +++ b/internal/roles/README.md @@ -1,93 +1,102 @@ # roles -RBAC(Role-Based Access Control) 기능을 제공합니다. - -역할 정의 관리와 파일 접근 권한 검증을 담당합니다. +RBAC(Role-Based Access Control) 기능을 제공하는 패키지입니다. 역할 정의 관리와 파일 접근 권한 검증을 담당합니다. ## 패키지 구조 ``` roles/ -├── roles.go # 역할 정의 관리 (Load/Save, Get/Set) -├── rbac.go # RBAC 권한 검증 +├── roles.go # 역할 파일 관리 (Load/Save), 역할 상태 관리 (Get/Set) +├── rbac.go # RBAC 권한 검증 (파일 권한 체크, 패턴 매칭) └── README.md ``` +- **roles.go**: `.sym/roles.json` 파일의 읽기/쓰기와 `.sym/.env`의 현재 역할 상태 관리 +- **rbac.go**: `user-policy.json`의 AllowWrite/DenyWrite 패턴을 기반으로 파일 수정 권한 검증 + ## 의존성 ### 패키지 사용자 | 위치 | 용도 | |------|------| -| `internal/cmd/init.go` | 초기 역할 생성 | -| `internal/cmd/my_role.go` | 역할 선택 CLI | -| `internal/cmd/dashboard.go` | 대시보드 역할 확인 | -| `internal/server/server.go` | 웹 대시보드 역할 API | -| `internal/mcp/server.go` | MCP RBAC 컨텍스트 | -| `internal/validator/validator.go` | 검증 시 RBAC 권한 확인 | +| `internal/cmd/init.go` | 초기 역할 생성, `SetCurrentRole("admin")` 설정 | +| `internal/cmd/my_role.go` | 역할 선택 CLI 인터페이스 | +| `internal/cmd/dashboard.go` | 대시보드 시작 전 역할 존재 여부 확인 | +| `internal/server/server.go` | 웹 대시보드 역할 관련 API 엔드포인트 | +| `internal/mcp/server.go` | MCP 서버에서 RBAC 컨텍스트 메시지 생성 | +| `internal/validator/validator.go` | 검증 시 RBAC 파일 권한 확인 | +| `tests/integration/rbac_test.go` | RBAC 통합 테스트 | ### 패키지 의존성 ``` - ┌──────────┐ - │ roles │ - └────┬─────┘ - │ - ┌─────┼─────┐ - ▼ ▼ ▼ -┌────────┐ ┌────────┐ ┌────────────┐ -│util/env│ │util/git│ │ policy │ -└────────┘ └────────┘ └────────────┘ - │ - ▼ - ┌────────────┐ - │ pkg/schema │ - └────────────┘ + ┌──────────┐ + │ roles │ + └────┬─────┘ + │ + ┌────────┼────────┐ + ▼ ▼ ▼ +┌────────┐ ┌────────┐ ┌────────┐ +│util/env│ │util/git│ │ policy │ +└────────┘ └────────┘ └───┬────┘ + │ + ▼ + ┌────────────┐ + │ pkg/schema │ + └────────────┘ ``` -## Public / Private API +| 패키지 | 용도 | +|--------|------| +| `internal/util/env` | `.env` 파일 읽기/쓰기 | +| `internal/util/git` | Git 저장소 루트 경로 탐지 | +| `internal/policy` | `user-policy.json` 로더 | +| `pkg/schema` | `UserPolicy`, `UserRole`, `UserRBAC` 타입 정의 | + +## Public/Private API ### Public API #### Types -| 타입 | 파일 | 설명 | +| 타입 | 위치 | 설명 | |------|------|------| -| `Roles` | roles.go:15 | `map[string][]string` - 역할별 사용자 목록 | -| `ValidationResult` | rbac.go:15 | RBAC 검증 결과 | +| `Roles` | roles.go:15 | `map[string][]string` - 역할명을 키로, 사용자명 목록을 값으로 가지는 맵 | +| `ValidationResult` | rbac.go:15 | RBAC 검증 결과 (`Allowed bool`, `DeniedFiles []string`) | #### Functions - 역할 파일 관리 -| 함수 | 파일 | 설명 | +| 함수 | 위치 | 설명 | |------|------|------| -| `GetRolesPath()` | roles.go:39 | .sym/roles.json 경로 반환 | -| `LoadRoles()` | roles.go:48 | roles.json 로드 | -| `SaveRoles(roles)` | roles.go:71 | roles.json 저장 | -| `RolesExists()` | roles.go:110 | roles.json 존재 여부 | +| `GetRolesPath()` | roles.go:39 | `.sym/roles.json` 파일 경로 반환 | +| `LoadRoles()` | roles.go:48 | `roles.json` 파일을 읽어 `Roles` 맵 반환 | +| `SaveRoles(roles Roles)` | roles.go:71 | `Roles` 맵을 `roles.json` 파일로 저장 | +| `RolesExists()` | roles.go:111 | `roles.json` 파일 존재 여부 확인 | #### Functions - 역할 상태 관리 -| 함수 | 파일 | 설명 | +| 함수 | 위치 | 설명 | |------|------|------| -| `GetCurrentRole()` | roles.go:129 | 현재 선택된 역할 (.sym/.env) | -| `SetCurrentRole(role)` | roles.go:139 | 현재 역할 설정 | -| `GetAvailableRoles()` | roles.go:149 | 사용 가능한 역할 목록 | -| `IsValidRole(role)` | roles.go:165 | 역할 유효성 검증 | +| `GetCurrentRole()` | roles.go:129 | `.sym/.env`에서 현재 선택된 역할 반환 | +| `SetCurrentRole(role string)` | roles.go:140 | 현재 역할을 `.sym/.env`에 저장 | +| `GetAvailableRoles()` | roles.go:151 | `roles.json`에 정의된 모든 역할명 목록 반환 (정렬됨) | +| `IsValidRole(role string)` | roles.go:166 | 역할명이 `roles.json`에 존재하는지 확인 | #### Functions - 사용자/권한 검증 -| 함수 | 파일 | 설명 | +| 함수 | 위치 | 설명 | |------|------|------| -| `GetUserRole(username)` | roles.go:91 | 사용자의 역할 조회 | -| `LoadUserPolicyFromRepo()` | rbac.go:30 | user-policy.json 로드 | -| `ValidateFilePermissionsForRole(role, files)` | rbac.go:132 | 역할별 파일 권한 검증 | +| `GetUserRole(username string)` | roles.go:92 | 사용자명으로 역할 조회 (없으면 `"none"` 반환) | +| `LoadUserPolicyFromRepo()` | rbac.go:30 | `user-policy.json` 파일 로드 | +| `ValidateFilePermissionsForRole(role string, files []string)` | rbac.go:132 | 역할의 파일 수정 권한 검증 | ### Private API -| 함수 | 파일 | 설명 | +| 함수 | 위치 | 설명 | |------|------|------| -| `getSymDir()` | roles.go:21 | .sym 디렉토리 경로 | -| `getEnvPath()` | roles.go:30 | .sym/.env 경로 | -| `getUserPolicyPath()` | rbac.go:21 | user-policy.json 경로 | -| `matchPattern(pattern, path)` | rbac.go:48 | glob 패턴 매칭 | -| `checkFilePermission(file, role)` | rbac.go:104 | 단일 파일 권한 확인 | +| `getSymDir()` | roles.go:21 | `.sym` 디렉토리 경로 반환 | +| `getEnvPath()` | roles.go:30 | `.sym/.env` 파일 경로 반환 | +| `getUserPolicyPath()` | rbac.go:21 | `user-policy.json` 파일 경로 반환 | +| `matchPattern(pattern, path string)` | rbac.go:48 | glob 패턴 매칭 (`**`, `*` 지원) | +| `checkFilePermission(filePath string, role *schema.UserRole)` | rbac.go:104 | 단일 파일 권한 확인 | diff --git a/internal/server/README.md b/internal/server/README.md index 5405e11..2691b8b 100644 --- a/internal/server/README.md +++ b/internal/server/README.md @@ -1,8 +1,93 @@ # server -웹 대시보드 HTTP 서버를 제공합니다. +Symphony 정책 편집기를 위한 웹 대시보드 HTTP 서버를 제공합니다. +역할 기반 접근 제어(RBAC), 정책 관리 REST API, 정적 파일 서빙 기능을 포함합니다. -정책 관리를 위한 REST API 및 정적 파일 제공 기능을 포함합니다. +## 패키지 구조 -**사용자**: cmd -**의존성**: config, git, github, policy, roles +``` +internal/server/ +├── server.go # 메인 서버 구현 (HTTP 핸들러, CORS, 권한 검사) +├── README.md # 패키지 문서 +└── static/ # 임베디드 웹 에셋 (go:embed) + ├── index.html # 웹 UI HTML (정책 편집기) + ├── policy-editor.js # 클라이언트 JavaScript (상태 관리, API 통신) + ├── styles/ # CSS 파일 + │ ├── input.css # Tailwind 소스 + │ └── output.css # 컴파일된 CSS + ├── icons/ # SVG 아이콘 (10개) + └── fonts/ # Pretendard 폰트 (Regular, Medium, Bold) +``` + +## 의존성 + +### 패키지 사용자 + +| 패키지 | 파일 | 용도 | +|--------|------|------| +| internal/cmd | dashboard.go | `sym dashboard` 명령어에서 서버 인스턴스 생성 및 실행 | + +### 패키지 의존성 + +| 패키지 | 용도 | +|--------|------| +| internal/converter | 정책 변환 (`/api/policy/convert` 엔드포인트) | +| internal/llm | LLM 프로바이더 생성 (정책 변환용) | +| internal/policy | 정책 파일 로드/저장, 템플릿 관리 | +| internal/roles | 역할 관리, 현재 역할 조회/설정 | +| internal/util/config | 프로젝트 설정(config.json) 로드/저장 | +| pkg/schema | UserPolicy 타입 정의 | + +**외부 의존성:** +- `github.com/pkg/browser` - 서버 시작 시 브라우저 자동 열기 + +## Public/Private API + +### Public API + +| 함수 | 시그니처 | 설명 | +|------|----------|------| +| NewServer | `NewServer(port int) (*Server, error)` | 지정된 포트로 서버 인스턴스 생성 | +| Start | `(s *Server) Start() error` | HTTP 서버 시작, 라우트 등록, 브라우저 열기 | + +**Server 구조체:** +```go +type Server struct { + port int +} +``` + +### Private API + +**미들웨어:** +- `corsMiddleware(next http.Handler) http.Handler` - CORS 헤더 추가 (모든 origin 허용) + +**권한 검사:** +- `hasPermissionForRole(role, permission string) (bool, error)` - config에서 정책 로드 후 권한 확인 +- `hasPermissionForRoleWithPolicy(role, permission string, policy *schema.UserPolicy) (bool, error)` - 제공된 정책으로 권한 확인 +- `checkPermissionForRole(userRole, permission string, policy *schema.UserPolicy) (bool, error)` - 권한 검사 핵심 로직 + +**HTTP 핸들러 (14개):** + +| 핸들러 | 라우트 | 설명 | +|--------|--------|------| +| handleGetMe | GET /api/me | 현재 역할 및 권한 조회 | +| handleSelectRole | POST /api/select-role | 역할 선택/변경 | +| handleAvailableRoles | GET /api/available-roles | 사용 가능한 역할 목록 | +| handleRoles | /api/roles | GET/POST 라우터 | +| handleGetRoles | GET /api/roles | roles.json 조회 | +| handleUpdateRoles | POST /api/roles | roles.json 업데이트 | +| handleProjectInfo | GET /api/project-info | 프로젝트 이름 조회 | +| handlePolicy | /api/policy | GET/POST 라우터 | +| handleGetPolicy | GET /api/policy | 정책 파일 조회 | +| handleSavePolicy | POST /api/policy | 정책 파일 저장 | +| handlePolicyPath | /api/policy/path | GET/POST 라우터 | +| handleSetPolicyPath | POST /api/policy/path | 정책 파일 경로 변경 | +| handlePolicyTemplates | GET /api/policy/templates | 템플릿 목록 조회 | +| handlePolicyTemplateDetail | GET /api/policy/templates/{name} | 특정 템플릿 상세 | +| handleUsers | GET /api/users | 모든 사용자 목록 | +| handleConvert | POST /api/policy/convert | 정책 변환 실행 | + +## 참고 문헌 + +- [Server REST API Reference](../../docs/server-api.md) - 엔드포인트 상세 문서 (요청/응답 스키마, 권한 모델) diff --git a/internal/validator/README.md b/internal/validator/README.md index 51d13ab..05d757f 100644 --- a/internal/validator/README.md +++ b/internal/validator/README.md @@ -1,106 +1,111 @@ # validator -코드 검증 오케스트레이터입니다. +Code validation orchestrator that validates code changes against policy using linters and LLM. -정책에 정의된 규칙을 바탕으로 린터와 LLM 엔진을 조율하고, 검증 결과를 수집하여 위반 사항을 보고합니다. +Implements a 4-phase pipeline: +1. RBAC permission checking +2. Rule grouping by engine +3. Execution unit creation +4. Parallel execution with concurrency control -## 패키지 구조 +## Package Structure ``` validator/ -├── validator.go # Validator 구조체, 4단계 파이프라인 -├── execution_unit.go # executionUnit 인터페이스 및 구현체 -├── llm_validator.go # LLM 기반 검증 로직 -├── llm_validator_test.go +├── validator.go # Main orchestrator, 4-phase validation pipeline +├── validator_test.go # Unit tests for validator +├── execution_unit.go # Execution unit interface and implementations +├── llm_validator.go # LLM-based validation logic +├── llm_validator_test.go # Unit tests for LLM validator └── README.md ``` -## 의존성 +## Dependencies -### 패키지 사용자 +### Package Users -| 위치 | 용도 | -|------|------| -| `internal/cmd/validate.go` | sym validate CLI 명령어 | -| `internal/mcp/server.go` | MCP validate_code 도구 | +| Location | Purpose | +|----------|---------| +| `internal/cmd/validate.go` | CLI `sym validate` command | +| `internal/mcp/server.go` | MCP `validate_code` tool | -### 패키지 의존성 +### Package Dependencies + +| Package | Purpose | +|---------|---------| +| `internal/linter` | Linter registry and execution | +| `internal/llm` | LLM provider interface | +| `internal/roles` | RBAC permission validation | +| `internal/util/git` | Git change types and diff utilities | +| `pkg/schema` | Policy and rule definitions | ``` ┌─────────────┐ │ validator │ └──────┬──────┘ - ┌────────────┬───┴───┬────────────┐ - ▼ ▼ ▼ ▼ - ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ - │ linter │ │ llm │ │ roles │ │ git │ - └──────────┘ └──────────┘ └──────────┘ └──────────┘ - │ - ▼ - ┌────────────┐ - │ pkg/schema │ - └────────────┘ + ┌────────────┬───┴───┬────────────┬────────────┐ + ▼ ▼ ▼ ▼ ▼ + ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ + │ linter │ │ llm │ │ roles │ │ util/git │ │ pkg/schema │ + └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────┘ ``` -## Public / Private API +## Public/Private API ### Public API #### Types -| 타입 | 파일 | 설명 | -|------|------|------| -| `Validator` | validator.go:37 | 검증 오케스트레이터 | -| `Violation` | validator.go:21 | 위반 사항 | -| `ValidationResult` | llm_validator.go:23 | 검증 결과 | -| `ValidationError` | llm_validator.go:16 | 엔진 실행 오류 | +| Type | File | Description | +|------|------|-------------| +| `Validator` | validator.go | Main validation orchestrator | +| `Violation` | validator.go | Represents a policy violation | +| `ValidationResult` | llm_validator.go | Aggregated validation results | +| `ValidationError` | llm_validator.go | Engine execution error | #### Constructors -| 함수 | 파일 | 설명 | -|------|------|------| -| `NewValidator(policy, verbose)` | validator.go:49 | Validator 생성 | -| `NewValidatorWithWorkDir(policy, verbose, workDir)` | validator.go:71 | 커스텀 workDir로 생성 | +| Function | Description | +|----------|-------------| +| `NewValidator(policy, verbose) *Validator` | Creates validator with current working directory | +| `NewValidatorWithWorkDir(policy, verbose, workDir) *Validator` | Creates validator with custom working directory | #### Methods -| 메서드 | 파일 | 설명 | -|--------|------|------| -| `(*Validator) SetLLMProvider(provider)` | validator.go:89 | LLM Provider 설정 | -| `(*Validator) ValidateChanges(ctx, changes)` | validator.go:280 | 검증 실행 | -| `(*Validator) Close()` | validator.go:413 | 리소스 정리 | +| Method | Description | +|--------|-------------| +| `(*Validator) SetLLMProvider(provider)` | Sets LLM provider for llm-validator rules | +| `(*Validator) ValidateChanges(ctx, changes) (*ValidationResult, error)` | Runs 4-phase validation pipeline | +| `(*Validator) Close() error` | Releases resources | ### Private API -#### Types +#### Interfaces -| 타입 | 파일 | 설명 | -|------|------|------| -| `llmValidator` | llm_validator.go:35 | LLM 전용 검증기 | -| `validationResponse` | llm_validator.go:241 | LLM 응답 파싱 결과 | -| `executionUnit` | execution_unit.go:21 | 실행 단위 인터페이스 | -| `linterExecutionUnit` | execution_unit.go:34 | 린터 실행 단위 | -| `llmExecutionUnit` | execution_unit.go:216 | LLM 실행 단위 | -| `ruleGroup` | validator.go:101 | 규칙 그룹화 | +| Interface | File | Description | +|-----------|------|-------------| +| `executionUnit` | execution_unit.go | Polymorphic execution contract | -#### Functions +#### Types -| 함수 | 파일 | 설명 | -|------|------|------| -| `newLLMValidator(provider, policy)` | llm_validator.go:41 | llmValidator 생성 | -| `getEngineName(rule)` | validator.go:93 | 규칙에서 엔진명 추출 | -| `getDefaultConcurrency()` | validator.go:109 | 기본 동시성 레벨 | -| `getLanguageFromFile(filePath)` | validator.go:420 | 파일 확장자로 언어 판별 | -| `parseValidationResponse(response)` | llm_validator.go:256 | LLM 응답 파싱 | -| `extractJSONField(response, field)` | llm_validator.go:353 | JSON 필드 추출 | +| Type | File | Description | +|------|------|-------------| +| `linterExecutionUnit` | execution_unit.go | Batches rules for single linter execution | +| `llmExecutionUnit` | execution_unit.go | Single (file, rule) pair for LLM validation | +| `llmValidator` | llm_validator.go | LLM-specific validation logic | +| `ruleGroup` | validator.go | Groups rules by engine for batching | +| `validationResponse` | llm_validator.go | Parsed LLM response structure | +| `jsonValidationResponse` | llm_validator.go | JSON deserialization target | -#### Methods +#### Functions -| 메서드 | 파일 | 설명 | -|--------|------|------| -| `(*Validator) groupRulesByEngine(...)` | validator.go:121 | 규칙 그룹화 | -| `(*Validator) createExecutionUnits(...)` | validator.go:179 | 실행 단위 생성 | -| `(*Validator) executeUnitsParallel(...)` | validator.go:221 | 병렬 실행 | -| `(*Validator) filterChangesForRule(...)` | validator.go:384 | 규칙별 변경 필터 | -| `(*llmValidator) Validate(...)` | llm_validator.go:54 | LLM 검증 실행 | -| `(*llmValidator) checkRule(...)` | llm_validator.go:153 | 단일 규칙 검증 | +| Function | File | Description | +|----------|------|-------------| +| `getEngineName(rule)` | validator.go | Extracts engine name from rule | +| `getDefaultConcurrency()` | validator.go | Returns CPU/2 bounded to [1,8] | +| `getLanguageFromFile(filePath)` | validator.go | Maps file extension to language | +| `newLLMValidator(provider, policy)` | llm_validator.go | Creates LLM validator instance | +| `parseValidationResponse(response)` | llm_validator.go | Parses LLM JSON response | +| `parseValidationResponseFallback(response)` | llm_validator.go | Fallback string-based parsing | +| `parseJSON(jsonStr, target)` | llm_validator.go | JSON unmarshaling wrapper | +| `extractJSONField(response, field)` | llm_validator.go | Manual JSON field extraction | From d7b71bd86cbd5a53ee3ce65e6cd366d750f9d932 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:45:58 +0000 Subject: [PATCH 06/13] docs: add util package documentation --- internal/util/README.md | 18 +++++++++++ internal/util/config/README.md | 59 ++++++++++++++++++++++++++++++++++ internal/util/env/README.md | 34 ++++++++++++++++++++ internal/util/git/README.md | 47 +++++++++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 internal/util/README.md create mode 100644 internal/util/config/README.md create mode 100644 internal/util/env/README.md create mode 100644 internal/util/git/README.md diff --git a/internal/util/README.md b/internal/util/README.md new file mode 100644 index 0000000..7b3bac1 --- /dev/null +++ b/internal/util/README.md @@ -0,0 +1,18 @@ +# util + +공통 유틸리티 패키지 모음 + +## 패키지 구조 + +``` +util/ +├── config/ # 설정 관리 +├── env/ # 환경 변수 관리 +└── git/ # Git 변경사항 감지 +``` + +## 하위 패키지 + +- [config](./config/README.md) - 설정 관리 (사용자 전역 + 프로젝트) +- [env](./env/README.md) - .env 파일 관리 +- [git](./git/README.md) - Git 변경사항 및 저장소 정보 diff --git a/internal/util/config/README.md b/internal/util/config/README.md new file mode 100644 index 0000000..6f136bd --- /dev/null +++ b/internal/util/config/README.md @@ -0,0 +1,59 @@ +# config + +설정 관리 패키지 (사용자 전역 설정 + 프로젝트 설정) + +## 패키지 구조 + +``` +config/ +├── config.go # 사용자 전역 설정 (~/.config/sym/config.json) +└── project.go # 프로젝트 레벨 설정 (.sym/config.json) +``` + +## 의존성 + +### 패키지 사용자 + +- `internal/cmd/init.go` - 프로젝트 초기화 시 설정 생성 +- `internal/cmd/convert.go` - 정책 경로 로드 +- `internal/cmd/llm.go` - LLM 프로바이더 설정 +- `internal/cmd/policy.go` - 정책 경로 관리 (사용자 전역 설정) +- `internal/mcp/server.go` - MCP 서버 설정 로드 +- `internal/server/server.go` - 대시보드 API 설정 +- `internal/llm/config.go` - LLM 설정 로드 + +### 패키지 의존성 + +- 없음 (외부 의존성만 사용) + +## Public API + +### 사용자 전역 설정 (config.go) + +| API | 설명 | +|-----|------| +| `Config` | 사용자 전역 설정 구조체 (`PolicyPath` 필드) | +| `LoadConfig()` | `~/.config/sym/config.json` 로드 | +| `SaveConfig(cfg)` | 설정 저장 | + +### 프로젝트 설정 (project.go) + +| API | 설명 | +|-----|------| +| `ProjectConfig` | 프로젝트 설정 구조체 | +| `LLMConfig` | LLM 설정 구조체 (`Provider`, `Model`) | +| `MCPConfig` | MCP 설정 구조체 (`Tools`) | +| `LoadProjectConfig()` | `.sym/config.json` 로드 | +| `SaveProjectConfig(cfg)` | 설정 저장 | +| `ProjectConfigExists()` | 설정 파일 존재 확인 | +| `GetProjectConfigPath()` | 설정 파일 경로 반환 | +| `GetProjectEnvPath()` | `.env` 파일 경로 반환 | +| `UpdateProjectConfigLLM(provider, model)` | LLM 설정 업데이트 | +| `UpdateProjectConfigMCP(tools)` | MCP 설정 업데이트 | + +## Private API + +| API | 설명 | +|-----|------| +| `init()` | 전역 설정 경로 초기화 (config.go) | +| `ensureConfigDir()` | 설정 디렉터리 생성 (config.go) | diff --git a/internal/util/env/README.md b/internal/util/env/README.md new file mode 100644 index 0000000..c201173 --- /dev/null +++ b/internal/util/env/README.md @@ -0,0 +1,34 @@ +# env + +환경 변수 및 `.env` 파일 관리 패키지 + +## 패키지 구조 + +``` +env/ +└── env.go # .env 파일 읽기/쓰기 +``` + +## 의존성 + +### 패키지 사용자 + +- `internal/cmd/llm.go` - API 키 저장 +- `internal/roles/roles.go` - 현재 역할 저장/로드 +- `internal/llm/openaiapi/provider.go` - API 키 로드 + +### 패키지 의존성 + +- 없음 (외부 의존성만 사용) + +## Public API + +| API | 설명 | +|-----|------| +| `GetAPIKey(keyName)` | 환경변수 또는 `.sym/.env`에서 API 키 조회 | +| `LoadKeyFromEnvFile(path, key)` | `.env` 파일에서 특정 키 로드 | +| `SaveKeyToEnvFile(path, key, value)` | `.env` 파일에 키-값 저장 | + +## Private API + +없음 diff --git a/internal/util/git/README.md b/internal/util/git/README.md new file mode 100644 index 0000000..42014fa --- /dev/null +++ b/internal/util/git/README.md @@ -0,0 +1,47 @@ +# git + +Git 변경사항 감지 및 저장소 정보 패키지 + +## 패키지 구조 + +``` +git/ +├── changes.go # Git 변경사항 감지 +├── changes_test.go # 테스트 +└── repo.go # 저장소 정보 조회 +``` + +## 의존성 + +### 패키지 사용자 + +- `internal/cmd/init.go` - 저장소 루트 확인 +- `internal/cmd/validate.go` - 변경사항 검증 +- `internal/cmd/mcp.go` - 저장소 루트 확인 +- `internal/mcp/server.go` - MCP 검증 시 변경사항 조회 +- `internal/validator/validator.go` - 변경사항 필터링 +- `internal/validator/llm_validator.go` - diff 추출 +- `internal/validator/execution_unit.go` - 추가된 라인 추출 +- `internal/roles/rbac.go` - 저장소 루트 확인 +- `internal/policy/manager.go` - 정책 파일 경로 구성 +- `tests/integration/*` - 통합 테스트 +- `tests/e2e/*` - E2E 테스트 + +### 패키지 의존성 + +- 없음 (외부 의존성만 사용) + +## Public API + +| API | 설명 | +|-----|------| +| `Change` | Git 변경 정보 구조체 (`FilePath`, `Status`, `Diff`) | +| `GetChanges()` | 모든 미커밋 변경사항 조회 (staged + unstaged + untracked) | +| `GetStagedChanges()` | 스테이징된 변경사항만 조회 | +| `ExtractAddedLines(diff)` | diff에서 추가된 라인만 추출 | +| `GetRepoRoot()` | Git 저장소 루트 경로 | +| `GetCurrentUser()` | 현재 Git 사용자 이름 | + +## Private API + +없음 From 43fa70e5492875a72aa99e149c9b402052f4aae2 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:46:08 +0000 Subject: [PATCH 07/13] docs: improve npm and schema documentation --- docs/command.md | 1 + npm/README.md | 99 ++++++++++---- pkg/schema/README.md | 303 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 378 insertions(+), 25 deletions(-) diff --git a/docs/command.md b/docs/command.md index 1fae471..fd2dbeb 100644 --- a/docs/command.md +++ b/docs/command.md @@ -715,3 +715,4 @@ sym llm test # 설정 안내 보기 sym llm setup ``` + diff --git a/npm/README.md b/npm/README.md index fa0588f..d2104ca 100644 --- a/npm/README.md +++ b/npm/README.md @@ -1,25 +1,59 @@ -# Symphony MCP Server +# Symphony -LLM-friendly convention linter for AI coding tools. +**LLM-Friendly Convention Linter for AI Coding Tools** -## Quick Start +Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 코드 컨벤션 검사기입니다. +간단한 설정만으로 프로젝트 규칙을 일관되게 적용하고, LLM 코드 생성 품질을 극대화할 수 있습니다. + +--- + +## 목차 + +- [Symphony](#symphony) + - [목차](#목차) + - [주요 기능](#주요-기능) + - [빠른 시작](#빠른-시작) + - [MCP 설정](#mcp-설정) + - [사용 가능한 MCP 도구](#사용-가능한-mcp-도구) + - [`query_conventions`](#query_conventions) + - [`validate_code`](#validate_code) + - [컨벤션 파일](#컨벤션-파일) + - [요구사항](#요구사항) + - [지원 플랫폼](#지원-플랫폼) + - [라이선스](#라이선스) + +--- + +## 주요 기능 + +- 자연어로 컨벤션 정의 +- LLM이 MCP를 통해 필요한 컨벤션만 추출하여 컨텍스트에 포함 +- LLM이 MCP를 통해 코드 변경사항에 대한 컨벤션 준수 여부를 검사 +- RBAC 기반 접근 제어 + +--- + +## 빠른 시작 ```bash -# 1. Install +# 1. CLI 설치 npm install -g @dev-symphony/sym -# 2. Initialize (GitHub OAuth login + MCP auto-setup) -sym login +# 2. 프로젝트 초기화 (.sym/ 폴더 생성 + MCP 설정) sym init -``` -> **Note**: `OPENAI_API_KEY` environment variable is required for LLM-based convention conversion. +# 3. 대시보드 실행 및 컨벤션 편집 +sym dashboard + +# 4. MCP 서버를 LLM IDE 내부에서 사용 +``` -## MCP Configuration +--- -MCP is auto-configured during `sym init`. +## MCP 설정 -For manual setup: +`sym init` 명령은 MCP 서버 구성을 자동으로 설정합니다. +만약 수동으로 설정하고 싶다면 아래를 `~/.config/.../config.json` 등에 추가하세요. ```json { @@ -32,19 +66,32 @@ For manual setup: } ``` -### Available Tools +--- -**query_conventions** -- Query project conventions by category, files, or languages -- All parameters are optional +## 사용 가능한 MCP 도구 -**validate_code** -- Validate code against defined conventions -- Parameters: files (required) +### `query_conventions` -## Policy File +- 프로젝트 컨벤션을 조회합니다. +- 카테고리, 파일 목록, 언어 등의 파라미터는 모두 optional입니다. -Create `.sym/user-policy.json` in your project root: +### `validate_code` + +- 코드가 정의된 규칙을 따르는지 검사합니다. +- 필수 파라미터: `files` + +--- + +## 컨벤션 파일 + +Symphony는 프로젝트 컨벤션을 **정책 파일(`.sym/user-policy.json`)**로 관리합니다. +아래 명령으로 대시보드를 열어 쉽게 편집할 수 있습니다. + +```bash +sym dashboard +``` + +예시 정책 파일: ```json { @@ -63,17 +110,23 @@ Create `.sym/user-policy.json` in your project root: } ``` -## Requirements +--- + +## 요구사항 - Node.js >= 16.0.0 - Policy file: `.sym/user-policy.json` -## Supported Platforms +--- + +## 지원 플랫폼 - macOS (Intel, Apple Silicon) - Linux (x64, ARM64) - Windows (x64) -## License +--- + +## 라이선스 MIT diff --git a/pkg/schema/README.md b/pkg/schema/README.md index 2deb0d2..287d1b6 100644 --- a/pkg/schema/README.md +++ b/pkg/schema/README.md @@ -4,5 +4,304 @@ UserPolicy(A Schema) 및 CodePolicy(B Schema) 타입을 정의하며, 전체 시스템의 데이터 흐름을 담당합니다. -**사용자**: cmd, converter, llm, mcp, policy, roles, server, validator -**의존성**: 없음 +## 패키지 구조 + +``` +pkg/schema/ +├── types.go # 모든 스키마 타입 정의 +└── README.md +``` + +## 의존성 + +### 패키지 사용자 + +| 패키지 | 용도 | +|--------|------| +| `internal/cmd` | CLI 명령어에서 정책 로드/저장 | +| `internal/converter` | A→B 스키마 변환 | +| `internal/llm` | LLM 응답 파싱 | +| `internal/mcp` | MCP 서버 정책 처리 | +| `internal/policy` | 정책 파일 로드/저장 | +| `internal/roles` | RBAC 검증 | +| `internal/server` | 웹 대시보드 API | +| `internal/validator` | 코드 검증 | + +### 패키지 의존성 + +- 없음 (독립 패키지) + +## 스키마 관계 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ A Schema (사용자 친화적) │ +│ UserPolicy ──┬── UserRBAC ─── UserRole │ +│ ├── UserDefaults │ +│ └── UserRule[] │ +└────────────────────────────┬────────────────────────────────────┘ + │ + converter.Convert() + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ B Schema (시스템 실행용) │ +│ CodePolicy ──┬── PolicyRBAC ─┬─ PolicyRole │ +│ │ └─ Permission ─ PermissionCond. │ +│ ├── ProjectInfo │ +│ ├── PolicyRule[] ─┬─ Selector │ +│ │ └─ Remedy │ +│ └── EnforceSettings ─ RBACEnforce │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## A Schema (사용자 입력용) + +사용자가 자연어로 컨벤션을 정의하기 위한 간소화된 스키마입니다. + +### UserPolicy + +메인 정책 구조체입니다. + +```go +type UserPolicy struct { + Version string `json:"version,omitempty"` // 정책 버전 (예: "1.0.0") + RBAC *UserRBAC `json:"rbac,omitempty"` // 역할 기반 접근 제어 + Defaults *UserDefaults `json:"defaults,omitempty"` // 규칙 기본값 + Rules []UserRule `json:"rules"` // 자연어 규칙 목록 +} +``` + +### UserRBAC + +RBAC 설정을 담는 구조체입니다. + +```go +type UserRBAC struct { + Roles map[string]UserRole `json:"roles"` // 역할명 → 역할 정의 +} +``` + +### UserRole + +단일 역할의 권한을 정의합니다. + +```go +type UserRole struct { + AllowWrite []string `json:"allowWrite,omitempty"` // 쓰기 허용 경로 (glob) + DenyWrite []string `json:"denyWrite,omitempty"` // 쓰기 거부 경로 (glob) + AllowExec []string `json:"allowExec,omitempty"` // 실행 허용 경로 (glob) + CanEditPolicy bool `json:"canEditPolicy,omitempty"` // 정책 편집 권한 + CanEditRoles bool `json:"canEditRoles,omitempty"` // 역할 편집 권한 +} +``` + +### UserDefaults + +규칙의 기본값을 정의합니다. + +```go +type UserDefaults struct { + Languages []string `json:"languages,omitempty"` // 기본 대상 언어 + DefaultLanguage string `json:"defaultLanguage,omitempty"` // 기본 언어 + Include []string `json:"include,omitempty"` // 포함 경로 + Exclude []string `json:"exclude,omitempty"` // 제외 경로 + Severity string `json:"severity,omitempty"` // 기본 심각도 + Autofix bool `json:"autofix,omitempty"` // 자동 수정 여부 +} +``` + +### UserRule + +자연어로 정의된 단일 규칙입니다. + +```go +type UserRule struct { + ID string `json:"id"` // 규칙 ID (필수) + Say string `json:"say"` // 자연어 규칙 설명 + Category string `json:"category,omitempty"` // 카테고리 (naming, formatting 등) + Languages []string `json:"languages,omitempty"` // 적용 언어 + Include []string `json:"include,omitempty"` // 포함 경로 + Exclude []string `json:"exclude,omitempty"` // 제외 경로 + Severity string `json:"severity,omitempty"` // 심각도 (error, warning, info) + Autofix bool `json:"autofix,omitempty"` // 자동 수정 여부 + Params map[string]any `json:"params,omitempty"` // 추가 파라미터 + Message string `json:"message,omitempty"` // 위반 시 메시지 + Example string `json:"example,omitempty"` // 예시 +} +``` + +## B Schema (시스템 실행용) + +린터 및 검증기가 실행하기 위한 정형화된 스키마입니다. + +### CodePolicy + +메인 검증 정책 구조체입니다. + +```go +type CodePolicy struct { + Version string `json:"version"` // 정책 버전 + Project *ProjectInfo `json:"project,omitempty"` // 프로젝트 메타데이터 + Extends []string `json:"extends,omitempty"` // 상속할 정책 URI + RBAC *PolicyRBAC `json:"rbac,omitempty"` // RBAC 설정 + Rules []PolicyRule `json:"rules"` // 정형화된 규칙 목록 + Enforce EnforceSettings `json:"enforce"` // 집행 설정 +} +``` + +### ProjectInfo + +프로젝트 메타데이터입니다. + +```go +type ProjectInfo struct { + Name string `json:"name,omitempty"` // 프로젝트 이름 + Languages []string `json:"languages,omitempty"` // 사용 언어 + Frameworks []string `json:"frameworks,omitempty"` // 프레임워크 +} +``` + +### PolicyRBAC / PolicyRole / Permission + +정책 스키마용 RBAC 구조체입니다. + +```go +type PolicyRBAC struct { + Roles map[string]PolicyRole `json:"roles"` // 역할명 → 역할 정의 +} + +type PolicyRole struct { + Inherits []string `json:"inherits,omitempty"` // 상속할 역할 + Permissions []Permission `json:"permissions"` // 권한 목록 +} + +type Permission struct { + Path string `json:"path"` // 경로 (glob) + Read bool `json:"read"` // 읽기 권한 + Write bool `json:"write"` // 쓰기 권한 + Execute bool `json:"execute"` // 실행 권한 + Conditions *PermissionConditions `json:"conditions,omitempty"` // 조건 +} + +type PermissionConditions struct { + Branches []string `json:"branches,omitempty"` // 적용 브랜치 + Time *TimeRange `json:"time,omitempty"` // 시간 범위 +} + +type TimeRange struct { + Start string `json:"start,omitempty"` // 시작 시간 (HH:MM:SS) + End string `json:"end,omitempty"` // 종료 시간 (HH:MM:SS) +} +``` + +### PolicyRule + +정형화된 단일 규칙입니다. + +```go +type PolicyRule struct { + ID string `json:"id"` // 규칙 ID + Enabled bool `json:"enabled"` // 활성화 여부 + Category string `json:"category"` // 카테고리 + Severity string `json:"severity"` // 심각도 + Desc string `json:"desc,omitempty"` // 설명 + When *Selector `json:"when,omitempty"` // 적용 조건 + Check map[string]any `json:"check"` // 검사 설정 (engine 필드 포함) + Remedy *Remedy `json:"remedy,omitempty"` // 자동 수정 설정 + Message string `json:"message,omitempty"` // 위반 시 메시지 +} +``` + +### Selector + +규칙 적용 조건을 정의합니다. + +```go +type Selector struct { + Languages []string `json:"languages,omitempty"` // 대상 언어 + Include []string `json:"include,omitempty"` // 포함 경로 + Exclude []string `json:"exclude,omitempty"` // 제외 경로 + Branches []string `json:"branches,omitempty"` // 대상 브랜치 + Roles []string `json:"roles,omitempty"` // 대상 역할 + Tags []string `json:"tags,omitempty"` // 태그 +} +``` + +### Remedy + +자동 수정 설정입니다. + +```go +type Remedy struct { + Autofix bool `json:"autofix"` // 자동 수정 활성화 + Tool string `json:"tool,omitempty"` // 수정 도구 (prettier, black 등) + Config map[string]any `json:"config,omitempty"` // 도구 설정 +} +``` + +### EnforceSettings / RBACEnforce + +집행 설정입니다. + +```go +type EnforceSettings struct { + Stages []string `json:"stages"` // 집행 단계 (pre-commit, ci 등) + FailOn []string `json:"fail_on,omitempty"` // 실패 조건 심각도 + RBACConfig *RBACEnforce `json:"rbac,omitempty"` // RBAC 집행 설정 +} + +type RBACEnforce struct { + Enabled bool `json:"enabled"` // RBAC 활성화 + Stages []string `json:"stages,omitempty"` // RBAC 집행 단계 + OnViolation string `json:"on_violation,omitempty"` // 위반 시 동작 (block, warn) +} +``` + +## 사용 예시 + +### A Schema 예시 + +```json +{ + "version": "1.0.0", + "rbac": { + "roles": { + "admin": { "allowWrite": ["**/*"], "canEditPolicy": true }, + "developer": { "allowWrite": ["src/**"], "denyWrite": [".sym/**"] } + } + }, + "defaults": { + "languages": ["typescript", "python"], + "severity": "warning" + }, + "rules": [ + { "id": "1", "say": "클래스 이름은 PascalCase", "category": "naming" }, + { "id": "2", "say": "한 줄은 100자 이하", "params": { "max": 100 } } + ] +} +``` + +### B Schema 예시 + +```json +{ + "version": "1.0", + "rules": [ + { + "id": "NAMING-CLASS-PASCAL", + "enabled": true, + "category": "naming", + "severity": "warning", + "check": { + "engine": "eslint", + "rule": "@typescript-eslint/naming-convention" + } + } + ], + "enforce": { + "stages": ["pre-commit", "ci"], + "fail_on": ["error"] + } +} +``` From ab2786bfbc6b2f0b6a000ca7c749555c7a46540e Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:46:27 +0000 Subject: [PATCH 08/13] chore: bump version to 0.1.9 in package.json --- npm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/package.json b/npm/package.json index d37f1fa..9b369c9 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "@dev-symphony/sym", - "version": "0.1.8", + "version": "0.1.9", "description": "Symphony - LLM-friendly convention linter for AI coding assistants", "keywords": [ "mcp", From b6aa147256712410ab8b2ac5f078096d94cb3d55 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 01:47:36 +0000 Subject: [PATCH 09/13] docs: remove outdated AGENTS.md file containing project guidelines and best practices --- AGENTS.md | 122 ------------------------------------------------------ 1 file changed, 122 deletions(-) delete mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index c2e9384..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,122 +0,0 @@ -# 저장소 가이드라인 - -## 프로젝트 구조 및 모듈 구성 -- 명확한 최상위 레이아웃을 사용하세요: - - `src/` — 애플리케이션 및 라이브러리 코드 - - `tests/` — `src/` 레이아웃을 반영한 자동화 테스트 - - `scripts/` — 개발/빌드/테스트 보조 스크립트 - - `assets/` — 정적 파일(이미지, JSON, 스키마) - - `docs/` — 설계 노트 및 ADR -- 모듈은 작고 응집력 있게 유지하고, 필요 시 레이어보다 도메인 기준으로 그룹화하세요. - -- Go 프로젝트 구조(뱅크샐러드 권장 예시): - - 루트에 `go.mod` 유지(모듈 경로는 소문자·단수형 권장). - - 최소 폴더만 사용해 단순성을 유지합니다. - - `cmd/` — 초기화/의존성 주입 등 실행 진입점 - - `config/` — 환경 변수 및 설정 - - `client/` — 외부 서비스/서드파티 클라이언트 - - `server/` — 서버 구현(미들웨어/핸들러/DB 등) - - `server/handler/` — RPC/엔드포인트별 핸들러 파일 - - `server/db/mysql/` — 자동 생성된 MySQL 모델 등 - - `server/server.go` — 라우팅/미들웨어/핸들러 배선 - - 필요 시 `internal/`, `api/`, 각 패키지의 `testdata/` 사용 가능하나, 과도한 폴더 분리는 지양합니다. - - 패키지/파일명은 소문자 단수형, 불필요한 언더스코어/대문자/축약 지양. - - 파일은 멀티 모듈 형태로 각 기능별로 구성되어 있어야 해 - -## 빌드, 테스트, 개발 명령어 -- 재현성을 위해 Make 타깃을 선호하세요: - - `make build` — 현재 플랫폼용 빌드 - - `make build-all` — 모든 플랫폼용 빌드 - - `make test` — 전체 테스트 스위트 실행 - - `make lint` — 린터/포매터 실행 -- 서비스 사전 요구사항(DB, 큐, 환경 변수)은 `docs/`에 문서화하세요. - -- Go 전용 권장 명령(가능하면 Make 타깃으로 래핑): - - 개발: `go run ./cmd/` 또는 해당 패키지. - - 테스트: `go test ./... -race -cover`(필요 시 `-shuffle=on`). - - 린트/정적 분석: `golangci-lint run`, `go vet ./...`. - - 보안 점검(선택): `govulncheck ./...`. - - 빌드: `go build -trimpath -ldflags "-s -w" ./cmd/`. - -## 코딩 스타일 및 네이밍 규칙 -- 들여쓰기: 공백 4칸. 라인 길이: 100. -- 네이밍: 모듈/파일은 `snake_case`, 클래스는 `PascalCase`, 함수/변수는 공개 여부에 따른 MixedCase (앞글자의 대소문자 여부), -- 함수는 약 50줄 이하로 유지하고, 순수하며 테스트 가능한 유닛을 선호하세요. - -- Go 스타일/프랙티스(뱅크샐러드 기준): - - 포맷팅/린트: `gofmt`/`gofumpt` + `goimports` 적용, `golangci-lint`와 `go vet` 필수. - - 인자 순서: `ctx context.Context`를 항상 맨 앞에 두고, 그 다음 DB/서비스 클라이언트. - 무거운 인자(slice/map)는 앞쪽, 가벼운 인자(userID, now 등)는 뒤쪽에 배치. - - 임포트 정렬: 표준 라이브러리 / 서드파티 / 사내(로컬) 순으로 세 그룹, 공백 줄로 구분. - `gci` 또는 `goimports-reviser` 사용 권장. - - 네이밍: 복수 결과는 `listXxxs`(단수는 `getXxx`) 사용. 모호한 단어(information, details, summary) 지양. - 상수는 camelCase(`defaultPageSize`), SCREAMING_SNAKE_CASE 사용 지양. - 패키지명은 `core/util/helper/common/infra` 등 범용명 피하고, 구체적으로(`parser`, `timeconv`, `testutil`). - - 파일 내 선언 순서: interface → type → const → var → new func → 공개 리시버 메서드 → 비공개 리시버 메서드 → 공개 함수 → 비공개 함수. - 테스트 함수 네이밍은 `TestPublicFunc`, `Test_privateFunc`, `TestType_Method` 패턴 준수. - - 에러/패닉: 런타임(요청 처리 중)엔 `panic`/`fatal` 사용 금지. 초기화(main 등)에서만 허용. - 패닉 가능 함수는 `MustXxx` 접두사 사용하고 테스트/초기화에서만 호출. - 서버에는 recovery 미들웨어/인터셉터 체인 구성. - - 컨텍스트: `context.Background()`를 기본(top-level)로, `context.TODO()`는 미정/미적용 시에만. - `Context`는 첫 인자(`ctx`)로 전달하고 타임아웃/취소를 전파. - - 시간/타임존: 함수 인자는 `time.Duration` 사용. 환경값은 조기에 Duration으로 변환. - 타임존은 초기화 시 `time.LoadLocation`으로 로드하여 재사용(`MustLoadKST()`). - - 문자열/유니코드: 문자열은 `range`로 rune 단위 순회, 길이는 `utf8.RuneCountInString`으로 계산. - - 동시성: goroutine은 누수 없게 설계, 에러 집계/취소 전파에 `x/sync/errgroup` 활용. - 복수 에러가 필요하면 `go-multierror` 등 사용 고려. - - 리시버 선택: 작은 불변 타입은 값 리시버, 그 외 포인터 리시버. 리시버 식별자는 짧고 일관되게. - - 코멘트: Godoc 스타일의 완전한 문장으로 공개 식별자/패키지 문서화. - - 리플렉션/원숭이패치: 핸들러 경로에서 `reflect` 사용 자제. 사이드 이펙트는 의존성 주입으로 대체. - - 함수 옵션: 선택적 인자는 Functional Options 패턴 사용. - - 이니셜리즘: ID, URL, HTTP, JSON, XML 등은 대문자 유지(예: `userID`, `serviceURL`). - - 인터페이스: 소비자(사용자) 패키지에서 정의하고, 작고 응집력 있게 유지. 불필요한 공개 인터페이스 지양, 가능한 한 구체 타입을 인자/반환값으로 사용. - - 에러 처리: `fmt.Errorf("%s: %w", op, err)`로 감싸기(wrap) 및 `errors.Is/As` 사용. 센티넬 에러는 `var`로 선언하고 문자열 비교 지양. 에러 메시지는 소문자로 시작, 말미에 구두점 생략. - - 로깅: 구조적 로깅 사용(zap/zerolog 등). 라이브러리 레이어에선 로깅 최소화하고 에러 반환 선호. 요청 단위 상관키(request-id/user-id) 포함, 비밀/PII는 로그 금지. 레벨/필드 일관성 유지. - - 컨텍스트 추가 수칙: `Context`를 구조체에 저장하지 않기, nil 컨텍스트 금지, 타입 지정 키 사용, 값은 작게 유지, 데드라인/취소 전파, 선택적 인자 전달용으로 사용 금지. - - 제로 값: 타입은 제로 값이 안전하고 유용하도록 설계. 생성자(`NewXxx`)는 불변식 설정에 사용하되 기본 사용에 강제하지 않기. - -## 테스트 가이드라인 -- `tests/`에서 `src/` 구조를 반영하세요(예: `src/foo/service.py` → `tests/foo/test_service.py`). -- Python: pytest(`tests/test_*.py`). Node: Jest/Vitest(`**/*.test.ts`). -- 변경된 코드의 라인 커버리지는 80% 이상을 목표로 하고, 엣지 케이스와 에러 경로를 포함하세요. -- 기본적으로 빠른 단위 테스트를 작성하고, 느림/통합 테스트는 명시적으로 표시하세요. - -- Go 테스트 지침(뱅크샐러드 기준): - - 테이블 주도 테스트 + `t.Run` 서브테스트. 가능하면 `t.Parallel()`로 병렬화. - - 어설션은 `stretchr/testify`의 `assert`/`require`를 선호. `suite` 패키지는 사용하지 않음. - - 결정적 테스트: map 순회 의존 로직 회피. JSON 비교는 `assert.JSONEq` 또는 `cmp.Diff` 사용. - 직렬화는 하드코딩 문자열 대신 헬퍼(`mustMarshal`)로 생성. - - 공용 헬퍼는 `t.Helper()`. 테스트 데이터는 `testdata/` 디렉터리에 보관. - - 벤치마크: `BenchmarkXxx(b *testing.B)`, 예제: `ExampleXxx()`로 문서/검증 겸용. - - 커버리지: `go test ./... -race -coverprofile=coverage.out`. - - 테스트 로깅: 테스트 로그는 캡처하거나 비활성화하여 노이즈 최소화. 필요한 경우 구조를 기준으로 단언. - - 에러 단언: `require.Error`와 `errors.Is/As`로 센티넬/래핑 에러를 검증. - -## 커밋 및 PR 가이드라인 -- Conventional Commits 사용: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`. -- 커밋은 작고 집중적으로, 명령형 제목과 간단한 근거 설명 본문을 포함하세요. -- PR에는 요약, 관련 이슈 링크, 필요 시 스크린샷/로그, 그리고 영향 항목 체크리스트(마이그레이션, 설정, 문서)를 포함하세요. - -## 보안 및 구성 -- 시크릿은 절대 커밋하지 마세요. `.env.example`을 사용하고 필요한 변수를 문서화하세요. -- 모든 입력을 검증하고 정제하세요. 파라미터화된 쿼리를 선호하세요. -- 토큰/키의 권한은 최소화하고 정기적으로 교체하세요. - -- 네트워크/서버 설정(권장): - - HTTP 서버: `ReadHeaderTimeout`, `ReadTimeout`, `WriteTimeout`, `IdleTimeout`, `MaxHeaderBytes`를 합리적으로 설정. - - HTTP 클라이언트: `Client.Timeout` 설정 및 `Transport` 튜닝(`IdleConnTimeout`, `MaxIdleConns`, `MaxIdleConnsPerHost`). 요청 단위 컨텍스트 데드라인 지정. - - gRPC: recovery/로깅/메트릭 인터셉터 구성, keepalive/메시지 크기 제한 설정, 호출 측에서 데드라인 강제. - - 재시도: 멱등(idempotent) 호출에 한해 백오프/지터 포함 재시도. 컨텍스트 취소/데드라인 전파. - -- Go 보안/정적 분석: - - `go vet`, `golangci-lint`, `staticcheck`(선택), `govulncheck`를 CI에 포함. - - `exec.Command`는 인자 분리로 쉘 인젝션 방지. `http.Server`에 합리적 타임아웃 설정. - - 크립토는 `crypto/rand` 사용(비밀번호/토큰 등). `math/rand`는 비보안용에 한정. - -## 에이전트 전용 메모 -- 변경 범위를 좁게 유지하고, 관련 없는 코드 리팩터링은 하지 마세요. -- 검색에는 `rg`를 선호하고, 파일은 250줄 이하의 청크로 읽으세요. -- 새로 생성하는 파일에도 이 문서의 지침을 따르세요. - -## 테스트 -테스트는 반드시 작성해야 하고, 변경된 코드의 라인 커버리지 80% 이상을 목표로 해야 합니다. \ No newline at end of file From fb9e6849d3d3ab687c59003e30052d92653986b6 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 02:25:21 +0000 Subject: [PATCH 10/13] chore: add golangci-lint configuration and update Makefile and CI workflow --- .github/workflows/ci.yml | 35 ++++++++++++++++------------------- .golangci.yml | 19 +++++++++++++++++++ Makefile | 4 ++-- 3 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 .golangci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3fe2fc6..1f40a1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,27 +15,24 @@ permissions: actions: write jobs: - # 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 + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 - # - name: Run go vet - # run: go vet ./... + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.1' + cache: true - # - 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@v9 + with: + version: v2.7.2 + args: --timeout=5m test: name: Unit Test diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8b1eae3 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,19 @@ +# golangci-lint v2 configuration +# Reference: https://github.com/golangci/golangci-lint/blob/main/.golangci.reference.yml + +version: "2" + +linters: + # 기본 린터 세트 사용 (errcheck, govet, ineffassign, staticcheck, unused) + default: standard + +formatters: + enable: + - goimports # import 정리 및 포맷팅 + +run: + # 타임아웃 설정 + timeout: 5m + + # 테스트 파일 포함 + tests: true diff --git a/Makefile b/Makefile index 3120a7c..8081c8a 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ clean: @rm -f coverage.out coverage.html fmt: - @go fmt ./... + @golangci-lint fmt lint: @golangci-lint run @@ -56,7 +56,7 @@ tidy: setup: @go mod download - @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 + @go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.7.2 run: @go run $(MAIN_PATH) $(ARGS) From 73720e28512156ce89c86dedbcdd7fcf29ddc79c Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 02:31:12 +0000 Subject: [PATCH 11/13] chore: update test command in Makefile to generate HTML coverage report and update documentation --- Makefile | 3 ++- docs/CONTRIBUTING.md | 14 ++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 8081c8a..6802c15 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,8 @@ build-all: @GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe $(MAIN_PATH) test: - @go test -v -race -coverprofile=coverage.out ./... + @go test -v -race -coverprofile=coverage.out -covermode=atomic ./... + @go tool cover -html=coverage.out -o coverage.html unit-test: @go test -short ./... diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d238460..966095f 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -76,10 +76,10 @@ make test | `make setup` | 개발 환경 초기화: Go 종속성 다운로드 및 golangci-lint v2.4.0 설치 | | `make build` | 현재 플랫폼용 바이너리 빌드. 출력: `bin/sym` | | `make build-all` | 모든 플랫폼용 빌드 (Linux amd64/arm64, macOS amd64/arm64, Windows amd64) | -| `make test` | 레이스 감지와 함께 모든 테스트 실행 및 커버리지 리포트 생성 (`coverage.out`) | +| `make test` | 레이스 감지와 함께 모든 테스트 실행 및 커버리지 리포트 생성 (`coverage.html`) | | `make unit-test` | 커버리지 없이 짧은 모드로 테스트 실행 (빠른 확인용) | | `make lint` | golangci-lint를 실행하여 코드 품질 검사 | -| `make fmt` | `go fmt`를 사용하여 모든 Go 소스 파일 포맷팅 | +| `make fmt` | `golangci-lint fmt`를 사용하여 모든 Go 소스 파일 포맷팅 | | `make tidy` | `go mod tidy`를 실행하여 모듈 종속성 정리 | | `make clean` | 빌드 아티팩트 제거 (`bin/`, `coverage.out`, `coverage.html`) | | `make run ARGS="..."` | 지정된 인자로 애플리케이션 빌드 및 실행 | @@ -458,7 +458,7 @@ Make 타겟을 사용하여 테스트 실행: ```bash # 레이스 감지 및 커버리지와 함께 모든 테스트 실행 -# coverage.out 파일 생성 +# coverage.html 파일 생성 make test # 커버리지 없이 빠른 테스트 실행 (개발 반복용으로 더 빠름) @@ -594,15 +594,9 @@ func TestConverter_ConvertRule(t *testing.T) { ### 커버리지 -`make test` 실행 후 커버리지 데이터가 `coverage.out`에 저장됩니다. +`make test` 실행 후 커버리지 데이터가 `coverage.html`에 저장됩니다. ```bash -# 터미널에서 커버리지 리포트 보기 -go tool cover -func=coverage.out - -# HTML 커버리지 리포트 생성 -go tool cover -html=coverage.out -o coverage.html - # 브라우저에서 열기 (macOS) open coverage.html ``` From 64a4fc6c990dc371a86317d1411a2413c316de91 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 02:35:48 +0000 Subject: [PATCH 12/13] fix: resolve golangci-lint warnings for errcheck and staticcheck --- internal/cmd/convert.go | 2 +- internal/cmd/llm.go | 1 - internal/linter/checkstyle/linter.go | 1 - internal/linter/eslint/linter_test.go | 1 - internal/linter/pmd/linter.go | 1 - internal/linter/pmd/parser.go | 22 +++--- internal/linter/prettier/linter.go | 1 - internal/linter/pylint/linter.go | 2 +- internal/linter/tsc/executor.go | 2 +- internal/linter/tsc/linter.go | 1 - internal/linter/tsc/linter_test.go | 1 - internal/linter/tsc/parser.go | 7 +- internal/llm/parser.go | 5 +- internal/mcp/server.go | 2 +- internal/server/server.go | 2 +- internal/util/config/project.go | 6 +- internal/validator/llm_validator_test.go | 1 - internal/validator/validator_test.go | 38 +++++----- pkg/schema/types.go | 2 +- tests/e2e/full_workflow_test.go | 6 +- tests/e2e/mcp_integration_test.go | 8 +-- tests/e2e/unit_workflow_test.go | 89 ++++++++++++------------ tests/e2e/validator_test.go | 8 +-- tests/integration/checkstyle_test.go | 8 +-- tests/integration/eslint_test.go | 10 +-- tests/integration/pmd_test.go | 10 +-- tests/integration/prettier_test.go | 10 +-- tests/integration/pylint_test.go | 8 +-- tests/integration/tsc_test.go | 10 +-- 29 files changed, 129 insertions(+), 136 deletions(-) diff --git a/internal/cmd/convert.go b/internal/cmd/convert.go index 4ea6286..e7e6318 100644 --- a/internal/cmd/convert.go +++ b/internal/cmd/convert.go @@ -85,7 +85,7 @@ func runNewConverter(userPolicy *schema.UserPolicy) error { if err != nil { return fmt.Errorf("no available LLM backend for convert: %w\nTip: configure provider in .sym/config.json", err) } - defer llmProvider.Close() + defer func() { _ = llmProvider.Close() }() // Create new converter conv := converter.NewConverter(llmProvider, convertOutputDir) diff --git a/internal/cmd/llm.go b/internal/cmd/llm.go index 05130ca..56b7adc 100644 --- a/internal/cmd/llm.go +++ b/internal/cmd/llm.go @@ -345,4 +345,3 @@ func ensureGitignore(path string) error { return nil } - diff --git a/internal/linter/checkstyle/linter.go b/internal/linter/checkstyle/linter.go index 4c3431a..ba2bbb6 100644 --- a/internal/linter/checkstyle/linter.go +++ b/internal/linter/checkstyle/linter.go @@ -134,7 +134,6 @@ func (l *Linter) Install(ctx context.Context, config linter.InstallConfig) error return nil } - // Execute runs Checkstyle with the given config and files. func (l *Linter) Execute(ctx context.Context, config []byte, files []string) (*linter.ToolOutput, error) { return l.execute(ctx, config, files) diff --git a/internal/linter/eslint/linter_test.go b/internal/linter/eslint/linter_test.go index 337e45b..a11ad27 100644 --- a/internal/linter/eslint/linter_test.go +++ b/internal/linter/eslint/linter_test.go @@ -164,4 +164,3 @@ func TestParseOutput(t *testing.T) { t.Error("Expected violations to be parsed") } } - diff --git a/internal/linter/pmd/linter.go b/internal/linter/pmd/linter.go index 8551a35..78e494b 100644 --- a/internal/linter/pmd/linter.go +++ b/internal/linter/pmd/linter.go @@ -144,7 +144,6 @@ func (l *Linter) Install(ctx context.Context, config linter.InstallConfig) error return nil } - // Execute runs PMD with the given config and files. func (l *Linter) Execute(ctx context.Context, config []byte, files []string) (*linter.ToolOutput, error) { return l.execute(ctx, config, files) diff --git a/internal/linter/pmd/parser.go b/internal/linter/pmd/parser.go index dd6c493..3b413ab 100644 --- a/internal/linter/pmd/parser.go +++ b/internal/linter/pmd/parser.go @@ -9,9 +9,9 @@ import ( // PMDOutput represents the JSON output from PMD. type PMDOutput struct { - FormatVersion int `json:"formatVersion"` - PMDVersion string `json:"pmdVersion"` - Files []PMDFile `json:"files"` + FormatVersion int `json:"formatVersion"` + PMDVersion string `json:"pmdVersion"` + Files []PMDFile `json:"files"` ProcessingErrors []PMDProcessingError `json:"processingErrors"` } @@ -23,14 +23,14 @@ type PMDFile struct { // PMDViolation represents a single violation in PMD output. type PMDViolation struct { - BeginLine int `json:"beginLine"` - BeginColumn int `json:"beginColumn"` - EndLine int `json:"endLine"` - EndColumn int `json:"endColumn"` - Description string `json:"description"` - Rule string `json:"rule"` - RuleSet string `json:"ruleSet"` - Priority int `json:"priority"` + BeginLine int `json:"beginLine"` + BeginColumn int `json:"beginColumn"` + EndLine int `json:"endLine"` + EndColumn int `json:"endColumn"` + Description string `json:"description"` + Rule string `json:"rule"` + RuleSet string `json:"ruleSet"` + Priority int `json:"priority"` ExternalInfo string `json:"externalInfoUrl"` } diff --git a/internal/linter/prettier/linter.go b/internal/linter/prettier/linter.go index a2b2f10..23e8bcb 100644 --- a/internal/linter/prettier/linter.go +++ b/internal/linter/prettier/linter.go @@ -99,7 +99,6 @@ func (l *Linter) Install(ctx context.Context, config linter.InstallConfig) error return err } - // Execute runs Prettier with the given config and files. // mode: "check" or "write" func (l *Linter) Execute(ctx context.Context, config []byte, files []string) (*linter.ToolOutput, error) { diff --git a/internal/linter/pylint/linter.go b/internal/linter/pylint/linter.go index 0577da3..cc268a1 100644 --- a/internal/linter/pylint/linter.go +++ b/internal/linter/pylint/linter.go @@ -143,7 +143,7 @@ func (l *Linter) Install(ctx context.Context, config linter.InstallConfig) error errMsg = "venv creation failed (no error message)" } if strings.Contains(errMsg, "ensurepip") || strings.Contains(errMsg, "python3-venv") { - return fmt.Errorf("failed to create virtualenv: python3-venv package not installed. "+ + return fmt.Errorf("failed to create virtualenv: python3-venv package not installed. " + "On Debian/Ubuntu, run: sudo apt install python3-venv") } return fmt.Errorf("failed to create virtualenv: %s", errMsg) diff --git a/internal/linter/tsc/executor.go b/internal/linter/tsc/executor.go index b0404e3..81e8409 100644 --- a/internal/linter/tsc/executor.go +++ b/internal/linter/tsc/executor.go @@ -52,7 +52,7 @@ func (l *Linter) execute(ctx context.Context, config []byte, files []string) (*l return nil, fmt.Errorf("failed to create temp tsconfig: %w", err) } configPath := tmpFile.Name() - tmpFile.Close() // Close for WriteFile to reopen + _ = tmpFile.Close() // Close for WriteFile to reopen if err := os.WriteFile(configPath, updatedConfig, 0644); err != nil { return nil, fmt.Errorf("failed to write tsconfig: %w", err) diff --git a/internal/linter/tsc/linter.go b/internal/linter/tsc/linter.go index 274bd48..3f6722b 100644 --- a/internal/linter/tsc/linter.go +++ b/internal/linter/tsc/linter.go @@ -113,7 +113,6 @@ func (l *Linter) Install(ctx context.Context, config linter.InstallConfig) error return nil } - // Execute runs tsc with the given config and files. // Returns type checking results. func (l *Linter) Execute(ctx context.Context, config []byte, files []string) (*linter.ToolOutput, error) { diff --git a/internal/linter/tsc/linter_test.go b/internal/linter/tsc/linter_test.go index aeb4082..aec95a1 100644 --- a/internal/linter/tsc/linter_test.go +++ b/internal/linter/tsc/linter_test.go @@ -180,4 +180,3 @@ src/app.ts(20,10): error TS2339: Property 'bar' does not exist on type 'Object'. t.Errorf("Expected 2 violations, got %d", len(violations)) } } - diff --git a/internal/linter/tsc/parser.go b/internal/linter/tsc/parser.go index fb6440b..0a2e951 100644 --- a/internal/linter/tsc/parser.go +++ b/internal/linter/tsc/parser.go @@ -20,13 +20,14 @@ type TSCDiagnostic struct { Category int `json:"category"` // 0=message, 1=error, 2=warning, 3=suggestion Code int `json:"code"` Message string `json:"messageText"` - Line int `json:"line"` // Custom field we add - Column int `json:"column"` // Custom field we add + Line int `json:"line"` // Custom field we add + Column int `json:"column"` // Custom field we add } // parseOutput parses tsc output and converts it to violations. // TSC output format (without --pretty): -// file.ts(line,col): error TS2304: Message here. +// +// file.ts(line,col): error TS2304: Message here. func parseOutput(output *linter.ToolOutput) ([]linter.Violation, error) { if output == nil { return []linter.Violation{}, nil diff --git a/internal/llm/parser.go b/internal/llm/parser.go index a9e53f9..5428c57 100644 --- a/internal/llm/parser.go +++ b/internal/llm/parser.go @@ -170,9 +170,10 @@ func findJSONBoundaries(s string) string { continue } - if c == startChar { + switch c { + case startChar: depth++ - } else if c == endChar { + case endChar: depth-- if depth == 0 { return s[start : i+1] diff --git a/internal/mcp/server.go b/internal/mcp/server.go index ecc4cc7..73d19aa 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -40,7 +40,7 @@ func ConvertPolicyWithLLM(userPolicyPath, codePolicyPath string) error { if err != nil { return fmt.Errorf("failed to create LLM provider: %w", err) } - defer llmProvider.Close() + defer func() { _ = llmProvider.Close() }() // Create converter with output directory outputDir := filepath.Dir(codePolicyPath) diff --git a/internal/server/server.go b/internal/server/server.go index 8f760a8..8fdff78 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -688,7 +688,7 @@ func (s *Server) handleConvert(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("Failed to create LLM provider: %v", err), http.StatusInternalServerError) return } - defer llmProvider.Close() + defer func() { _ = llmProvider.Close() }() // Create converter with LLM provider and output directory conv := converter.NewConverter(llmProvider, outputDir) diff --git a/internal/util/config/project.go b/internal/util/config/project.go index 8e576d4..937f298 100644 --- a/internal/util/config/project.go +++ b/internal/util/config/project.go @@ -9,9 +9,9 @@ import ( // ProjectConfig represents the .sym/config.json structure type ProjectConfig struct { - LLM LLMConfig `json:"llm,omitempty"` - MCP MCPConfig `json:"mcp,omitempty"` - PolicyPath string `json:"policy_path,omitempty"` + LLM LLMConfig `json:"llm,omitempty"` + MCP MCPConfig `json:"mcp,omitempty"` + PolicyPath string `json:"policy_path,omitempty"` } // LLMConfig holds LLM provider settings diff --git a/internal/validator/llm_validator_test.go b/internal/validator/llm_validator_test.go index 9e25908..2e6997b 100644 --- a/internal/validator/llm_validator_test.go +++ b/internal/validator/llm_validator_test.go @@ -118,4 +118,3 @@ func TestExtractJSONField(t *testing.T) { }) } } - diff --git a/internal/validator/validator_test.go b/internal/validator/validator_test.go index 017305b..64f095f 100644 --- a/internal/validator/validator_test.go +++ b/internal/validator/validator_test.go @@ -3,8 +3,8 @@ package validator import ( "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" ) @@ -141,39 +141,39 @@ func TestParseJSON(t *testing.T) { func TestParseValidationResponseFallback(t *testing.T) { tests := []struct { - name string - response string - expectViolates bool + name string + response string + expectViolates bool expectConfidence string }{ { - name: "violates true", - response: `{"violates": true, "description": "test"}`, - expectViolates: true, + name: "violates true", + response: `{"violates": true, "description": "test"}`, + expectViolates: true, expectConfidence: "medium", }, { - name: "violates true no space", - response: `{"violates":true}`, - expectViolates: true, + name: "violates true no space", + response: `{"violates":true}`, + expectViolates: true, expectConfidence: "medium", }, { - name: "violates false", - response: `{"violates": false}`, - expectViolates: false, + name: "violates false", + response: `{"violates": false}`, + expectViolates: false, expectConfidence: "low", }, { - name: "does not violate text", - response: `The code does not violate the rule.`, - expectViolates: false, + name: "does not violate text", + response: `The code does not violate the rule.`, + expectViolates: false, expectConfidence: "low", }, { - name: "no violation indicators", - response: `Random text without any violation`, - expectViolates: false, + name: "no violation indicators", + response: `Random text without any violation`, + expectViolates: false, expectConfidence: "low", }, } diff --git a/pkg/schema/types.go b/pkg/schema/types.go index b28f99e..2b26c08 100644 --- a/pkg/schema/types.go +++ b/pkg/schema/types.go @@ -34,7 +34,7 @@ type UserDefaults struct { // UserRule represents a single rule in user schema type UserRule struct { - ID string `json:"id"` // Rule ID (required, can be number or string) + ID string `json:"id"` // Rule ID (required, can be number or string) Say string `json:"say"` Category string `json:"category,omitempty"` Languages []string `json:"languages,omitempty"` diff --git a/tests/e2e/full_workflow_test.go b/tests/e2e/full_workflow_test.go index 7449f25..73873fe 100644 --- a/tests/e2e/full_workflow_test.go +++ b/tests/e2e/full_workflow_test.go @@ -9,8 +9,8 @@ import ( "time" "github.com/DevSymphony/sym-cli/internal/converter" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/llm" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -171,7 +171,7 @@ func ProcessData(data string) error { llmValidator := validator.NewValidator(&convertedPolicy, false) llmValidator.SetLLMProvider(provider) - defer llmValidator.Close() + defer func() { _ = llmValidator.Close() }() // Validate BAD code t.Log("STEP 4a: Validating BAD code (should find violations)") @@ -340,7 +340,7 @@ func TestE2E_CodeGenerationFeedbackLoop(t *testing.T) { require.NoError(t, err, "LLM provider creation should succeed") v := validator.NewValidator(policy, false) v.SetLLMProvider(provider) - defer v.Close() + defer func() { _ = v.Close() }() ctx := context.Background() // Iteration 1: Bad code diff --git a/tests/e2e/mcp_integration_test.go b/tests/e2e/mcp_integration_test.go index c103f80..ddff94b 100644 --- a/tests/e2e/mcp_integration_test.go +++ b/tests/e2e/mcp_integration_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/llm" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -148,7 +148,7 @@ func TestMCP_ValidateAIGeneratedCode(t *testing.T) { // Create validator v := validator.NewValidator(policy, false) v.SetLLMProvider(provider) - defer v.Close() + defer func() { _ = v.Close() }() ctx := context.Background() // Test 1: Validate BAD code (should find multiple violations) @@ -261,7 +261,7 @@ func TestMCP_ValidateAIGeneratedCode(t *testing.T) { securityValidator := validator.NewValidator(securityPolicy, false) securityValidator.SetLLMProvider(provider) - defer securityValidator.Close() + defer func() { _ = securityValidator.Close() }() // Code with security violation (format as git diff with + prefix) codeWithSecurityIssue := `+const apiKey = "sk-1234567890abcdef"; // Hardcoded secret @@ -387,7 +387,7 @@ func TestMCP_EndToEndWorkflow(t *testing.T) { require.NoError(t, err, "LLM provider creation should succeed") v := validator.NewValidator(policy, false) v.SetLLMProvider(llmProvider) - defer v.Close() + defer func() { _ = v.Close() }() result, err := v.ValidateChanges(context.Background(), []git.Change{ {FilePath: "auth.js", Diff: generatedCode}, diff --git a/tests/e2e/unit_workflow_test.go b/tests/e2e/unit_workflow_test.go index 7b7b330..6f76f0b 100644 --- a/tests/e2e/unit_workflow_test.go +++ b/tests/e2e/unit_workflow_test.go @@ -88,34 +88,34 @@ func TestUnit_RuleFiltering(t *testing.T) { // TestUnit_ValidationResponseParsing tests parsing LLM validation responses func TestUnit_ValidationResponseParsing(t *testing.T) { tests := []struct { - name string - response string - expectViolates bool - expectDesc bool + name string + response string + expectViolates bool + expectDesc bool }{ { - name: "explicit violation", - response: `{"violates": true, "description": "Hardcoded secret detected"}`, - expectViolates: true, - expectDesc: true, + name: "explicit violation", + response: `{"violates": true, "description": "Hardcoded secret detected"}`, + expectViolates: true, + expectDesc: true, }, { - name: "no violation", - response: `{"violates": false}`, - expectViolates: false, - expectDesc: false, + name: "no violation", + response: `{"violates": false}`, + expectViolates: false, + expectDesc: false, }, { - name: "text-based violation", - response: `The code violates the security policy by hardcoding an API key.`, - expectViolates: true, - expectDesc: true, + name: "text-based violation", + response: `The code violates the security policy by hardcoding an API key.`, + expectViolates: true, + expectDesc: true, }, { - name: "text-based no violation", - response: `The code does not violate any conventions.`, - expectViolates: false, - expectDesc: false, + name: "text-based no violation", + response: `The code does not violate any conventions.`, + expectViolates: false, + expectDesc: false, }, } @@ -130,10 +130,9 @@ func TestUnit_ValidationResponseParsing(t *testing.T) { hasJSONNoViolation := contains(tt.response, "\"violates\": false") // Check for text-based violation (but exclude negations like "does not violate") - hasTextViolation := !hasJSONNoViolation && ( - contains(tt.response, "violates the") || - contains(tt.response, "violates any") || - (contains(tt.response, "violate") && !contains(tt.response, "does not violate") && !contains(tt.response, "not violate"))) + hasTextViolation := !hasJSONNoViolation && (contains(tt.response, "violates the") || + contains(tt.response, "violates any") || + (contains(tt.response, "violate") && !contains(tt.response, "does not violate") && !contains(tt.response, "not violate"))) containsViolation := hasJSONViolation || hasTextViolation @@ -152,7 +151,7 @@ func TestUnit_ValidationResponseParsing(t *testing.T) { func TestUnit_WorkflowSteps(t *testing.T) { t.Run("step1_user_creates_policy", func(t *testing.T) { policy := schema.UserPolicy{ - Version: "1.0.0", + Version: "1.0.0", Rules: []schema.UserRule{ {Say: "No hardcoded secrets", Category: "security"}, }, @@ -172,9 +171,9 @@ func TestUnit_WorkflowSteps(t *testing.T) { t.Run("step2_conversion_structure", func(t *testing.T) { // Test that conversion produces expected structure userRule := schema.UserRule{ - Say: "No hardcoded secrets", - Category: "security", - Severity: "error", + Say: "No hardcoded secrets", + Category: "security", + Severity: "error", } // After conversion, should have structured fields @@ -202,15 +201,15 @@ func TestUnit_WorkflowSteps(t *testing.T) { t.Run("step4_validation_result_structure", func(t *testing.T) { // Test validation result structure result := validator.ValidationResult{ - Checked: 5, - Passed: 3, - Failed: 2, + Checked: 5, + Passed: 3, + Failed: 2, Violations: []validator.Violation{ { - RuleID: "SEC-001", - Severity: "error", - Message: "Hardcoded secret detected", - File: "test.go", + RuleID: "SEC-001", + Severity: "error", + Message: "Hardcoded secret detected", + File: "test.go", }, }, } @@ -227,18 +226,18 @@ func TestUnit_MCPToolResponses(t *testing.T) { t.Run("get_conventions_by_category", func(t *testing.T) { // Simulated MCP tool response response := map[string]interface{}{ - "tool": "get_conventions_by_category", - "category": "security", + "tool": "get_conventions_by_category", + "category": "security", "conventions": []map[string]string{ { - "id": "SEC-001", - "message": "No hardcoded secrets", - "severity": "error", + "id": "SEC-001", + "message": "No hardcoded secrets", + "severity": "error", }, { - "id": "SEC-002", - "message": "Use parameterized queries", - "severity": "error", + "id": "SEC-002", + "message": "Use parameterized queries", + "severity": "error", }, }, } @@ -250,8 +249,8 @@ func TestUnit_MCPToolResponses(t *testing.T) { t.Run("get_all_conventions", func(t *testing.T) { response := map[string]interface{}{ - "tool": "get_all_conventions", - "count": 10, + "tool": "get_all_conventions", + "count": 10, "conventions": []string{ "No hardcoded secrets", "Document exported functions", diff --git a/tests/e2e/validator_test.go b/tests/e2e/validator_test.go index 5e93562..294dd83 100644 --- a/tests/e2e/validator_test.go +++ b/tests/e2e/validator_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/llm" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -39,7 +39,7 @@ func TestE2E_ValidatorWithPolicy(t *testing.T) { // Create validator v := validator.NewValidator(policy, false) v.SetLLMProvider(provider) - defer v.Close() + defer func() { _ = v.Close() }() // Create a test change (simulating git diff output) changes := []git.Change{ @@ -95,7 +95,7 @@ func TestE2E_ValidatorWithGoodCode(t *testing.T) { // Create validator v := validator.NewValidator(policy, false) v.SetLLMProvider(provider) - defer v.Close() + defer func() { _ = v.Close() }() // Create a test change with good code changes := []git.Change{ @@ -182,7 +182,7 @@ func TestE2E_ValidatorFilter(t *testing.T) { // Create validator v := validator.NewValidator(policy, false) v.SetLLMProvider(provider) - defer v.Close() + defer func() { _ = v.Close() }() // Test with Go file changes := []git.Change{ diff --git a/tests/integration/checkstyle_test.go b/tests/integration/checkstyle_test.go index 8604dcc..ef8a68a 100644 --- a/tests/integration/checkstyle_test.go +++ b/tests/integration/checkstyle_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestCheckstyle_ValidateChanges(t *testing.T) { // 3. Create validator with custom workDir v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() // 4. Check tool availability adp, err := linter.Global().GetLinter("checkstyle") @@ -113,7 +113,7 @@ func TestCheckstyle_NamingRules(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("checkstyle") if err != nil { @@ -200,7 +200,7 @@ func TestCheckstyle_ToolNameAndRuleID(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("checkstyle") if err != nil { diff --git a/tests/integration/eslint_test.go b/tests/integration/eslint_test.go index 211d577..99153cb 100644 --- a/tests/integration/eslint_test.go +++ b/tests/integration/eslint_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestESLint_ValidateChanges(t *testing.T) { // 3. Create validator with custom workDir v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() // 4. Check tool availability adp, err := linter.Global().GetLinter("eslint") @@ -112,7 +112,7 @@ func TestESLint_NamingConventions(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("eslint") if err != nil { @@ -169,7 +169,7 @@ func TestESLint_MaxLineLength(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("eslint") if err != nil { @@ -224,7 +224,7 @@ func TestESLint_ToolNameAndRuleID(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("eslint") if err != nil { diff --git a/tests/integration/pmd_test.go b/tests/integration/pmd_test.go index d198157..2f72a1f 100644 --- a/tests/integration/pmd_test.go +++ b/tests/integration/pmd_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestPMD_ValidateChanges(t *testing.T) { // 3. Create validator with custom workDir v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() // 4. Check tool availability adp, err := linter.Global().GetLinter("pmd") @@ -113,7 +113,7 @@ func TestPMD_EmptyCatchBlock(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("pmd") if err != nil { @@ -168,7 +168,7 @@ func TestPMD_UnusedPrivateMethod(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("pmd") if err != nil { @@ -223,7 +223,7 @@ func TestPMD_ToolNameAndRuleID(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("pmd") if err != nil { diff --git a/tests/integration/prettier_test.go b/tests/integration/prettier_test.go index 23dc00b..fcac1ae 100644 --- a/tests/integration/prettier_test.go +++ b/tests/integration/prettier_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestPrettier_ValidateChanges(t *testing.T) { // 3. Create validator with custom workDir v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() // 4. Check tool availability adp, err := linter.Global().GetLinter("prettier") @@ -113,7 +113,7 @@ func TestPrettier_FormattingCheck(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("prettier") if err != nil { @@ -173,7 +173,7 @@ func TestPrettier_QuotesAndIndent(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("prettier") if err != nil { @@ -226,7 +226,7 @@ func TestPrettier_ToolNameAndRuleID(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("prettier") if err != nil { diff --git a/tests/integration/pylint_test.go b/tests/integration/pylint_test.go index 7dea824..24ec9c8 100644 --- a/tests/integration/pylint_test.go +++ b/tests/integration/pylint_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestPylint_ValidateChanges(t *testing.T) { // 3. Create validator with custom workDir (symDir = workDir/.sym) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() // 4. Check tool availability adp, err := linter.Global().GetLinter("pylint") @@ -114,7 +114,7 @@ func TestPylint_NamingConventions(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("pylint") if err != nil { @@ -200,7 +200,7 @@ func TestPylint_ToolNameAndRuleID(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("pylint") if err != nil { diff --git a/tests/integration/tsc_test.go b/tests/integration/tsc_test.go index 8f6c4e3..1bda64c 100644 --- a/tests/integration/tsc_test.go +++ b/tests/integration/tsc_test.go @@ -8,8 +8,8 @@ import ( "strings" "testing" - "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/linter" + "github.com/DevSymphony/sym-cli/internal/util/git" "github.com/DevSymphony/sym-cli/internal/validator" "github.com/DevSymphony/sym-cli/pkg/schema" "github.com/stretchr/testify/assert" @@ -40,7 +40,7 @@ func TestTSC_ValidateChanges(t *testing.T) { // 3. Create validator with custom workDir v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() // 4. Check tool availability adp, err := linter.Global().GetLinter("tsc") @@ -108,7 +108,7 @@ func TestTSC_StrictNullChecks(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("tsc") if err != nil { @@ -166,7 +166,7 @@ func TestTSC_TypeErrors(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("tsc") if err != nil { @@ -219,7 +219,7 @@ func TestTSC_ToolNameAndRuleID(t *testing.T) { require.NoError(t, json.Unmarshal(policyData, &policy)) v := validator.NewValidatorWithWorkDir(&policy, true, testdataDir) - defer v.Close() + defer func() { _ = v.Close() }() adp, err := linter.Global().GetLinter("tsc") if err != nil { From 09ac10f629e7d2bc556860f1f2e5244d11ffa96e Mon Sep 17 00:00:00 2001 From: ikjeong Date: Wed, 10 Dec 2025 02:44:13 +0000 Subject: [PATCH 13/13] chore: remove verification step --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f40a1f..c1267f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,6 @@ jobs: uses: golangci/golangci-lint-action@v9 with: version: v2.7.2 - args: --timeout=5m test: name: Unit Test @@ -51,9 +50,6 @@ jobs: - name: Download dependencies run: go mod download - - name: Verify dependencies - run: go mod verify - - name: Run tests run: go test -short -v -race -coverprofile=coverage.out -covermode=atomic ./...