From 8643a2d0a829755d7b74fd6dae3d7e1ea2c5f132 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Fri, 12 Dec 2025 09:09:25 +0000 Subject: [PATCH 1/5] refactor: extract UpdateDefaultsLanguages to policy package --- internal/importer/README.md | 3 ++- internal/importer/importer.go | 21 +-------------------- internal/policy/README.md | 6 +++++- internal/policy/defaults.go | 28 ++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 internal/policy/defaults.go diff --git a/internal/importer/README.md b/internal/importer/README.md index 8a8be46..bf78761 100644 --- a/internal/importer/README.md +++ b/internal/importer/README.md @@ -123,7 +123,8 @@ importer/ |-------------|------| | `assignUniqueIDs(existing, extracted, result)` | 고유 ID 생성 및 중복 처리 | | `generateUniqueID(baseID, existingIDs)` | 고유 규칙 ID 생성 | -| `updateDefaultsLanguages(policy, newRules)` | defaults.languages 업데이트 | + +**참고**: `defaults.languages` 업데이트는 `policy.UpdateDefaultsLanguages()` 공용 함수로 처리됩니다. #### Extractor (extractor.go) diff --git a/internal/importer/importer.go b/internal/importer/importer.go index 381563f..30be673 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -81,7 +81,7 @@ func (i *Importer) Import(ctx context.Context, input *ImportInput) (*ImportResul result.RulesAdded = newRules // Step 5.5: Update defaults.languages with new languages from rules - i.updateDefaultsLanguages(existingPolicy, newRules) + policy.UpdateDefaultsLanguages(existingPolicy, newRules) // Step 6: Save updated policy if err := policy.SavePolicy(existingPolicy, ""); err != nil { @@ -152,22 +152,3 @@ func (i *Importer) generateUniqueID(baseID string, existingIDs map[string]bool) counter++ } } - -// updateDefaultsLanguages adds new languages from rules to defaults.languages -func (i *Importer) updateDefaultsLanguages(p *schema.UserPolicy, newRules []schema.UserRule) { - // Build set of existing default languages - existingLangs := make(map[string]bool) - for _, lang := range p.Defaults.Languages { - existingLangs[lang] = true - } - - // Collect new languages from rules - for _, rule := range newRules { - for _, lang := range rule.Languages { - if !existingLangs[lang] { - p.Defaults.Languages = append(p.Defaults.Languages, lang) - existingLangs[lang] = true - } - } - } -} diff --git a/internal/policy/README.md b/internal/policy/README.md index 6fafd74..bc91a87 100644 --- a/internal/policy/README.md +++ b/internal/policy/README.md @@ -10,6 +10,7 @@ UserPolicy(A 스키마)와 CodePolicy(B 스키마) 파일을 로드하고, 정 internal/policy/ ├── loader.go # 정책 파일 로더 (Loader 구조체) ├── manager.go # 정책 관리 함수 (경로, 로드, 저장, 검증) +├── defaults.go # defaults.languages 자동 업데이트 함수 ├── templates.go # 템플릿 관리 (embed.FS 기반) ├── README.md └── templates/ # 내장 정책 템플릿 (7개) @@ -30,9 +31,11 @@ internal/policy/ |--------|------|-----------| | cmd | policy.go | 정책 경로 표시, 검증 CLI 명령 | | cmd | init.go | 프로젝트 초기화 시 기본 정책 생성 | +| cmd | convention.go | 컨벤션 추가/편집 시 언어 자동 업데이트 | | roles | rbac.go | RBAC 검증을 위한 정책 로드 | | server | server.go | 대시보드 REST API (정책 CRUD, 템플릿) | -| mcp | server.go | MCP 서버 정책 로드 및 변환 | +| mcp | server.go | MCP 서버 정책 로드, 변환, add/edit_convention 시 언어 자동 업데이트 | +| importer | importer.go | Import 시 언어 자동 업데이트 | ### 패키지 의존성 @@ -78,6 +81,7 @@ type Template struct { | `PolicyExists(customPath) (bool, error)` | manager.go:133 | 정책 파일 존재 여부 | | `GetTemplates() ([]Template, error)` | templates.go:26 | 템플릿 목록 반환 | | `GetTemplate(name) (*UserPolicy, error)` | templates.go:81 | 특정 템플릿 로드 | +| `UpdateDefaultsLanguages(policy, rules)` | defaults.go:9 | 규칙에서 언어 추출하여 defaults.languages에 추가 | ### Private API diff --git a/internal/policy/defaults.go b/internal/policy/defaults.go new file mode 100644 index 0000000..cf23e3d --- /dev/null +++ b/internal/policy/defaults.go @@ -0,0 +1,28 @@ +package policy + +import "github.com/DevSymphony/sym-cli/pkg/schema" + +// UpdateDefaultsLanguages adds new languages from rules to defaults.languages. +// This function is used by the importer and convention add/edit operations +// to automatically track languages used in the project. +func UpdateDefaultsLanguages(p *schema.UserPolicy, rules []schema.UserRule) { + if p.Defaults == nil { + p.Defaults = &schema.UserDefaults{} + } + + // Build set of existing default languages + existingLangs := make(map[string]bool) + for _, lang := range p.Defaults.Languages { + existingLangs[lang] = true + } + + // Collect new languages from rules + for _, rule := range rules { + for _, lang := range rule.Languages { + if !existingLangs[lang] { + p.Defaults.Languages = append(p.Defaults.Languages, lang) + existingLangs[lang] = true + } + } + } +} From d7d3d1a79f4fd5bf8eb719aabcf676049c9b8dc4 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Fri, 12 Dec 2025 09:09:41 +0000 Subject: [PATCH 2/5] feat: add convention CRUD operations - rename query_conventions to list_convention - add add_convention, edit_convention, remove_convention MCP tools - add sym convention list|add|edit|remove CLI commands - auto-update defaults.languages on convention add/edit --- README.md | 25 +- docs/ARCHITECTURE.md | 5 +- docs/COMMAND.md | 256 ++++++++++++++- internal/cmd/README.md | 10 + internal/cmd/convention.go | 601 ++++++++++++++++++++++++++++++++++++ internal/mcp/README.md | 16 +- internal/mcp/server.go | 373 +++++++++++++++++++++- internal/mcp/server_test.go | 14 +- npm/README.md | 33 +- 9 files changed, 1300 insertions(+), 33 deletions(-) create mode 100644 internal/cmd/convention.go diff --git a/README.md b/README.md index b4f961e..3f88739 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,16 @@ Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 - [빠른 시작](#빠른-시작) - [MCP 설정](#mcp-설정) - [사용 가능한 MCP 도구](#사용-가능한-mcp-도구) - - [`query_conventions`](#query_conventions) + - [`list_convention`](#list_convention) - [`validate_code`](#validate_code) - [`list_category`](#list_category) - [`add_category`](#add_category) - [`edit_category`](#edit_category) - [`remove_category`](#remove_category) - [`import_convention`](#import_convention) + - [`add_convention`](#add_convention) + - [`edit_convention`](#edit_convention) + - [`remove_convention`](#remove_convention) - [컨벤션 파일](#컨벤션-파일) - [요구사항](#요구사항) - [지원 플랫폼](#지원-플랫폼) @@ -76,10 +79,10 @@ sym dashboard ## 사용 가능한 MCP 도구 -### `query_conventions` +### `list_convention` - 프로젝트 컨벤션을 조회합니다. -- 카테고리, 파일 목록, 언어 등의 파라미터는 모두 optional입니다. +- 카테고리, 언어 등의 파라미터는 모두 optional입니다. ### `validate_code` @@ -113,6 +116,22 @@ sym dashboard - 필수 파라미터: `path` - 선택 파라미터: `mode` (`append` 또는 `clear`, 기본값: `append`) +### `add_convention` + +- 새 컨벤션(규칙)을 추가합니다 (배치 지원). +- 필수 파라미터: `conventions` (배열) +- 컨벤션에 포함된 언어는 자동으로 `defaults.languages`에 추가됩니다. + +### `edit_convention` + +- 기존 컨벤션을 편집합니다 (배치 지원). +- 필수 파라미터: `edits` (배열) + +### `remove_convention` + +- 컨벤션을 삭제합니다 (배치 지원). +- 필수 파라미터: `ids` (배열) + --- ## 컨벤션 파일 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8f4294d..366b5c7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -140,13 +140,16 @@ AI 코딩 도구(Claude Code, Cursor 등)와 stdio를 통해 통신합니다. | Tool | Description | |------|-------------| -| `query_conventions` | 프로젝트 컨벤션 조회 | +| `list_convention` | 프로젝트 컨벤션 조회 | | `validate_code` | 코드 변경사항 검증 | | `list_category` | 카테고리 목록 조회 | | `add_category` | 카테고리 추가 (배치 지원) | | `edit_category` | 카테고리 편집 (배치 지원) | | `remove_category` | 카테고리 삭제 (배치 지원) | | `import_convention` | 외부 문서에서 컨벤션 추출 | +| `add_convention` | 컨벤션 추가 (배치 지원) | +| `edit_convention` | 컨벤션 편집 (배치 지원) | +| `remove_convention` | 컨벤션 삭제 (배치 지원) | #### HTTP Server (`internal/server`) diff --git a/docs/COMMAND.md b/docs/COMMAND.md index 32b2eb3..ace3ce2 100644 --- a/docs/COMMAND.md +++ b/docs/COMMAND.md @@ -39,13 +39,16 @@ Symphony (`sym`)는 코드 컨벤션 관리와 RBAC(역할 기반 접근 제어) - [MCP 통합](#mcp-통합) - [지원 도구](#지원-도구) - [MCP 도구 스키마](#mcp-도구-스키마) - - [query\_conventions](#query_conventions) + - [list\_convention](#list_convention) - [validate\_code](#validate_code) - [list\_category](#list_category) - [add\_category](#add_category) - [edit\_category](#edit_category) - [remove\_category](#remove_category) - [import\_convention](#import_convention) + - [add\_convention](#add_convention) + - [edit\_convention](#edit_convention) + - [remove\_convention](#remove_convention) - [등록 방법](#등록-방법) - [LLM 프로바이더](#llm-프로바이더) - [지원 프로바이더](#지원-프로바이더) @@ -112,7 +115,8 @@ sym ├── convert # 정책 → 린터 설정 변환 ├── validate # Git 변경사항 검증 ├── import # 외부 문서에서 컨벤션 추출 -├── category # 카테고리 목록 조회 +├── category # 카테고리 관리 +├── convention # 컨벤션(규칙) 관리 ├── mcp # MCP 서버 실행 ├── llm # LLM 프로바이더 관리 │ ├── status # 현재 설정 확인 @@ -599,17 +603,188 @@ sym category remove -f names.json --- +### sym convention + +**설명**: 컨벤션(규칙)을 관리합니다. + +user-policy.json에 정의된 규칙을 조회, 추가, 편집, 삭제할 수 있습니다. 규칙은 코딩 컨벤션의 구체적인 내용을 정의합니다. + +**서브커맨드**: +- `list` - 컨벤션 목록 조회 +- `add` - 새 컨벤션 추가 +- `edit` - 기존 컨벤션 편집 +- `remove` - 컨벤션 삭제 + +**관련 파일**: `internal/cmd/convention.go` + +--- + +#### sym convention list + +**설명**: 모든 컨벤션을 표시합니다. + +**문법**: +``` +sym convention list [--category ] [--language ] +``` + +**플래그**: +- `--category`, `-c` - 특정 카테고리로 필터링 +- `--language`, `-l` - 특정 언어로 필터링 + +**출력 예시**: +``` +[Conventions] 5 rules available + + • [SEC-001] security (error) + Use parameterized queries for database operations + languages: go, python + + • [PERF-001] performance (warning) + Avoid N+1 queries in database operations +``` + +--- + +#### sym convention add + +**설명**: 새 컨벤션을 추가합니다. + +**문법**: +``` +sym convention add [flags] +sym convention add -f +``` + +**플래그**: +- `-f, --file` - 배치 추가를 위한 JSON 파일 +- `--category` - 카테고리 (선택) +- `--languages` - 언어 목록 (쉼표로 구분) +- `--severity` - 심각도: error, warning, info (기본값: warning) +- `--autofix` - 자동 수정 활성화 +- `--message` - 위반 시 표시할 메시지 +- `--example` - 코드 예시 +- `--include` - 포함할 파일 패턴 (쉼표로 구분) +- `--exclude` - 제외할 파일 패턴 (쉼표로 구분) + +**예시**: +```bash +# 단일 추가 +sym convention add SEC-001 "Use parameterized queries" --category security --languages go,python --severity error + +# 배치 추가 +sym convention add -f conventions.json +``` + +**배치 파일 형식** (`conventions.json`): +```json +[ + { + "id": "SEC-001", + "say": "Use parameterized queries", + "category": "security", + "languages": ["go", "python"], + "severity": "error" + } +] +``` + +**참고**: 컨벤션에 포함된 언어는 자동으로 `defaults.languages`에 추가됩니다. + +--- + +#### sym convention edit + +**설명**: 기존 컨벤션을 편집합니다. + +**문법**: +``` +sym convention edit [flags] +sym convention edit -f +``` + +**플래그**: +- `-f, --file` - 배치 편집을 위한 JSON 파일 +- `--new-id` - 새 ID +- `--say` - 새 설명 +- `--category` - 새 카테고리 +- `--languages` - 새 언어 목록 +- `--severity` - 새 심각도 +- `--autofix` - 자동 수정 활성화/비활성화 +- `--message` - 새 메시지 +- `--example` - 새 예시 +- `--include` - 새 포함 패턴 +- `--exclude` - 새 제외 패턴 + +**예시**: +```bash +# 심각도 변경 +sym convention edit SEC-001 --severity warning + +# ID 변경 +sym convention edit SEC-001 --new-id SEC-001-v2 + +# 배치 편집 +sym convention edit -f edits.json +``` + +**배치 파일 형식** (`edits.json`): +```json +[ + {"id": "SEC-001", "severity": "warning"}, + {"id": "PERF-001", "new_id": "PERF-001-v2"} +] +``` + +--- + +#### sym convention remove + +**설명**: 컨벤션을 삭제합니다. + +**문법**: +``` +sym convention remove [ids...] +sym convention remove -f +``` + +**플래그**: +- `-f, --file` - 삭제할 컨벤션 ID가 담긴 JSON 파일 + +**예시**: +```bash +# 단일 삭제 +sym convention remove SEC-001 + +# 다중 삭제 +sym convention remove SEC-001 PERF-001 PERF-002 + +# 배치 삭제 +sym convention remove -f ids.json +``` + +**배치 파일 형식** (`ids.json`): +```json +["SEC-001", "PERF-001", "PERF-002"] +``` + +--- + ### sym mcp **설명**: MCP(Model Context Protocol) 서버를 시작합니다. LLM 기반 코딩 도구가 stdio를 통해 컨벤션을 쿼리하고 코드를 검증할 수 있습니다. **제공되는 MCP 도구**: -- `query_conventions`: 주어진 컨텍스트에 대한 컨벤션 쿼리 +- `list_convention`: 프로젝트 컨벤션 조회 - `validate_code`: 코드의 컨벤션 준수 여부 검증 - `list_category`: 사용 가능한 카테고리 목록 조회 - `add_category`: 카테고리 추가 (배치 지원) - `edit_category`: 카테고리 편집 (배치 지원) - `remove_category`: 카테고리 삭제 (배치 지원) +- `import_convention`: 외부 문서에서 컨벤션 추출 +- `add_convention`: 컨벤션(규칙) 추가 (배치 지원) +- `edit_convention`: 컨벤션(규칙) 편집 (배치 지원) +- `remove_convention`: 컨벤션(규칙) 삭제 (배치 지원) **통신 방식**: stdio (Claude Desktop, Claude Code, Cursor 등 MCP 클라이언트와 통합) @@ -837,9 +1012,9 @@ Symphony는 다음 AI 코딩 도구에 MCP 서버로 등록될 수 있습니다. ### MCP 도구 스키마 -#### query_conventions +#### list_convention -코딩 전 프로젝트 컨벤션을 쿼리합니다. +코딩 전 프로젝트 컨벤션을 조회합니다. **입력 스키마**: @@ -988,6 +1163,77 @@ Rules added (3): • [PERF-002] Use pagination for large data sets (performance) ``` +#### add_convention + +컨벤션(규칙)을 추가합니다 (배치 모드). + +**입력 스키마**: + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `conventions` | array | 예 | `{id, say, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?}` 객체 배열 | + +**예시**: +```json +{ + "conventions": [ + { + "id": "SEC-001", + "say": "Use parameterized queries for database operations", + "category": "security", + "languages": ["go", "python"], + "severity": "error" + }, + { + "id": "PERF-001", + "say": "Avoid N+1 queries in database operations", + "category": "performance" + } + ] +} +``` + +**참고**: 컨벤션에 포함된 언어는 자동으로 `defaults.languages`에 추가됩니다. + +#### edit_convention + +컨벤션(규칙)을 편집합니다 (배치 모드). + +**입력 스키마**: + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `edits` | array | 예 | `{id, new_id?, say?, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?}` 객체 배열 | + +**예시**: +```json +{ + "edits": [ + {"id": "SEC-001", "severity": "warning"}, + {"id": "PERF-001", "new_id": "PERF-001-v2", "say": "Updated description"} + ] +} +``` + +**참고**: 편집된 컨벤션에 새 언어가 추가되면 자동으로 `defaults.languages`에 추가됩니다. + +#### remove_convention + +컨벤션(규칙)을 삭제합니다 (배치 모드). + +**입력 스키마**: + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `ids` | array | 예 | 삭제할 컨벤션 ID 배열 | + +**예시**: +```json +{ + "ids": ["SEC-001", "PERF-001"] +} +``` + ### 등록 방법 ```bash diff --git a/internal/cmd/README.md b/internal/cmd/README.md index 1700af7..8c7f818 100644 --- a/internal/cmd/README.md +++ b/internal/cmd/README.md @@ -21,6 +21,7 @@ cmd/ ├── mcp.go # sym mcp 명령어 (MCP 서버) ├── mcp_register.go # MCP 서버 등록 헬퍼 함수 ├── category.go # sym category list|add|edit|remove 명령어 (카테고리 관리) +├── convention.go # sym convention list|add|edit|remove 명령어 (컨벤션 관리) ├── import.go # sym import 명령어 (외부 문서에서 컨벤션 추출) ├── survey_templates.go # 커스텀 survey UI 템플릿 └── README.md @@ -87,6 +88,11 @@ cmd/ | `llmSetupCmd` | llm.go:47 | llm setup 명령어 | | `mcpCmd` | mcp.go:15 | mcp 명령어 | | `categoryCmd` | category.go:10 | category 명령어 | +| `conventionCmd` | convention.go:44 | convention 명령어 | +| `conventionListCmd` | convention.go:60 | convention list 명령어 | +| `conventionAddCmd` | convention.go:68 | convention add 명령어 | +| `conventionEditCmd` | convention.go:93 | convention edit 명령어 | +| `conventionRemoveCmd` | convention.go:115 | convention remove 명령어 | | `importCmd` | import.go:18 | import 명령어 | #### 명령어 실행 함수 @@ -108,6 +114,10 @@ cmd/ | `runCategoryAdd(cmd, args)` | category.go:165 | category add 실행 | | `runCategoryEdit(cmd, args)` | category.go:240 | category edit 실행 | | `runCategoryRemove(cmd, args)` | category.go:353 | category remove 실행 | +| `runConventionList(cmd, args)` | convention.go:179 | convention list 실행 | +| `runConventionAdd(cmd, args)` | convention.go:215 | convention add 실행 | +| `runConventionEdit(cmd, args)` | convention.go:295 | convention edit 실행 | +| `runConventionRemove(cmd, args)` | convention.go:414 | convention remove 실행 | | `runImport(cmd, args)` | import.go:50 | import 실행 | #### 헬퍼 함수 - 초기화 diff --git a/internal/cmd/convention.go b/internal/cmd/convention.go new file mode 100644 index 0000000..01c1c89 --- /dev/null +++ b/internal/cmd/convention.go @@ -0,0 +1,601 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/DevSymphony/sym-cli/internal/policy" + "github.com/DevSymphony/sym-cli/internal/roles" + "github.com/DevSymphony/sym-cli/pkg/schema" + "github.com/spf13/cobra" +) + +// ConventionItem represents a convention for batch operations. +type ConventionItem struct { + ID string `json:"id"` + Say string `json:"say"` + Category string `json:"category,omitempty"` + Languages []string `json:"languages,omitempty"` + Severity string `json:"severity,omitempty"` + Autofix bool `json:"autofix,omitempty"` + Message string `json:"message,omitempty"` + Example string `json:"example,omitempty"` + Include []string `json:"include,omitempty"` + Exclude []string `json:"exclude,omitempty"` +} + +// ConventionEditItem represents a convention edit for batch operations. +type ConventionEditItem struct { + ID string `json:"id"` + NewID string `json:"new_id,omitempty"` + Say string `json:"say,omitempty"` + Category string `json:"category,omitempty"` + Languages []string `json:"languages,omitempty"` + Severity string `json:"severity,omitempty"` + Autofix *bool `json:"autofix,omitempty"` + Message string `json:"message,omitempty"` + Example string `json:"example,omitempty"` + Include []string `json:"include,omitempty"` + Exclude []string `json:"exclude,omitempty"` +} + +var conventionCmd = &cobra.Command{ + Use: "convention", + Short: "Manage conventions (rules)", + Long: `Manage conventions (rules) in user-policy.json. + +Conventions define coding standards and rules that are enforced during validation. + +Available subcommands: + list - List all conventions + add - Add a new convention + edit - Edit an existing convention + remove - Remove a convention`, +} + +var conventionListCmd = &cobra.Command{ + Use: "list", + Short: "List all conventions", + Long: `List all conventions with their ID, category, description, and severity. + +Conventions are defined in user-policy.json and can be customized by the user. +Run 'sym init' to create default conventions.`, + RunE: runConventionList, +} + +var conventionAddCmd = &cobra.Command{ + Use: "add [id] [say]", + Short: "Add a new convention", + Long: `Add a new convention (rule). + +Single mode: + sym convention add NAMING-001 "Use snake_case for variables" --category naming --languages python --severity error + +Batch mode (JSON file): + sym convention add -f conventions.json + + conventions.json format: + [ + { + "id": "NAMING-001", + "say": "Use snake_case for variables", + "category": "naming", + "languages": ["python"], + "severity": "error", + "message": "Variable names must use snake_case" + } + ]`, + Args: cobra.MaximumNArgs(2), + RunE: runConventionAdd, +} + +var conventionEditCmd = &cobra.Command{ + Use: "edit [id]", + Short: "Edit an existing convention", + Long: `Edit an existing convention's fields. + +Single mode: + sym convention edit NAMING-001 --say "Updated description" --severity warning + sym convention edit NAMING-001 --id NAMING-002 --category style + sym convention edit NAMING-001 --languages python,go + +Batch mode (JSON file): + sym convention edit -f edits.json + + edits.json format: + [ + {"id": "NAMING-001", "say": "Updated", "severity": "warning"}, + {"id": "STYLE-001", "new_id": "STYLE-002", "category": "formatting"} + ]`, + Args: cobra.MaximumNArgs(1), + RunE: runConventionEdit, +} + +var conventionRemoveCmd = &cobra.Command{ + Use: "remove [ids...]", + Short: "Remove a convention", + Long: `Remove conventions from user-policy.json. + +Single mode: + sym convention remove NAMING-001 + +Batch mode (multiple args): + sym convention remove NAMING-001 NAMING-002 STYLE-001 + +Batch mode (JSON file): + sym convention remove -f ids.json + + ids.json format: + ["NAMING-001", "NAMING-002", "STYLE-001"]`, + Args: cobra.ArbitraryArgs, + RunE: runConventionRemove, +} + +func init() { + rootCmd.AddCommand(conventionCmd) + + // Add subcommands + conventionCmd.AddCommand(conventionListCmd) + conventionCmd.AddCommand(conventionAddCmd) + conventionCmd.AddCommand(conventionEditCmd) + conventionCmd.AddCommand(conventionRemoveCmd) + + // Add command flags + conventionAddCmd.Flags().StringP("file", "f", "", "JSON file with conventions to add") + conventionAddCmd.Flags().String("category", "", "Category name") + conventionAddCmd.Flags().StringSlice("languages", nil, "Programming languages (comma-separated)") + conventionAddCmd.Flags().String("severity", "", "Severity level (error, warning, info)") + conventionAddCmd.Flags().Bool("autofix", false, "Enable auto-fix") + conventionAddCmd.Flags().String("message", "", "Message to display on violation") + conventionAddCmd.Flags().String("example", "", "Code example") + conventionAddCmd.Flags().StringSlice("include", nil, "File patterns to include") + conventionAddCmd.Flags().StringSlice("exclude", nil, "File patterns to exclude") + + // Edit command flags + conventionEditCmd.Flags().StringP("file", "f", "", "JSON file with convention edits") + conventionEditCmd.Flags().String("id", "", "New convention ID") + conventionEditCmd.Flags().String("say", "", "New description") + conventionEditCmd.Flags().String("category", "", "New category name") + conventionEditCmd.Flags().StringSlice("languages", nil, "New programming languages (comma-separated)") + conventionEditCmd.Flags().String("severity", "", "New severity level (error, warning, info)") + conventionEditCmd.Flags().Bool("autofix", false, "Enable auto-fix") + conventionEditCmd.Flags().String("message", "", "New message to display on violation") + conventionEditCmd.Flags().String("example", "", "New code example") + conventionEditCmd.Flags().StringSlice("include", nil, "New file patterns to include") + conventionEditCmd.Flags().StringSlice("exclude", nil, "New file patterns to exclude") + + // Remove command flags + conventionRemoveCmd.Flags().StringP("file", "f", "", "JSON file with convention IDs to remove") +} + +func runConventionList(cmd *cobra.Command, args []string) error { + // Load conventions from user-policy.json + userPolicy, err := roles.LoadUserPolicyFromRepo() + if err != nil { + printWarn("Failed to load user-policy.json") + fmt.Println("Run 'sym init' to create default conventions") + return nil + } + + rules := userPolicy.Rules + if len(rules) == 0 { + printWarn("No conventions defined in user-policy.json") + fmt.Println("Run 'sym init' to create default conventions or use 'sym convention add' to add new ones") + return nil + } + + printTitle("Conventions", fmt.Sprintf("%d conventions available", len(rules))) + fmt.Println() + + for _, rule := range rules { + // Format: ID [CATEGORY] (languages): description + languages := "" + if len(rule.Languages) > 0 { + languages = fmt.Sprintf(" (%s)", strings.Join(rule.Languages, ", ")) + } + category := "" + if rule.Category != "" { + category = fmt.Sprintf(" [%s]", rule.Category) + } + severity := rule.Severity + if severity == "" { + severity = "warning" + } + + fmt.Printf(" %s %s%s%s\n", colorize(bold, "•"), colorize(cyan, rule.ID), colorize(yellow, category), languages) + fmt.Printf(" %s\n", rule.Say) + fmt.Printf(" severity: %s\n\n", severity) + } + + return nil +} + +func runConventionAdd(cmd *cobra.Command, args []string) error { + fileFlag, _ := cmd.Flags().GetString("file") + + // Load policy + userPolicy, err := policy.LoadPolicy("") + if err != nil { + return fmt.Errorf("failed to load policy: %w", err) + } + + // Build existing IDs map + existingIDs := make(map[string]bool) + for _, rule := range userPolicy.Rules { + existingIDs[rule.ID] = true + } + + var conventions []ConventionItem + + if fileFlag != "" { + // Batch mode: load from JSON file + data, err := os.ReadFile(fileFlag) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + if err := json.Unmarshal(data, &conventions); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + if len(conventions) == 0 { + return fmt.Errorf("no conventions found in file") + } + } else { + // Single mode: use args and flags + if len(args) != 2 { + return fmt.Errorf("usage: sym convention add [--flags] or sym convention add -f ") + } + + category, _ := cmd.Flags().GetString("category") + languages, _ := cmd.Flags().GetStringSlice("languages") + severity, _ := cmd.Flags().GetString("severity") + autofix, _ := cmd.Flags().GetBool("autofix") + message, _ := cmd.Flags().GetString("message") + example, _ := cmd.Flags().GetString("example") + include, _ := cmd.Flags().GetStringSlice("include") + exclude, _ := cmd.Flags().GetStringSlice("exclude") + + conventions = []ConventionItem{{ + ID: args[0], + Say: args[1], + Category: category, + Languages: languages, + Severity: severity, + Autofix: autofix, + Message: message, + Example: example, + Include: include, + Exclude: exclude, + }} + } + + var succeeded []string + var failed []string + var addedRules []schema.UserRule + + // Process each convention + for _, conv := range conventions { + if conv.ID == "" { + failed = append(failed, "(empty): ID is required") + continue + } + if conv.Say == "" { + failed = append(failed, fmt.Sprintf("%s: say (description) is required", conv.ID)) + continue + } + if existingIDs[conv.ID] { + failed = append(failed, fmt.Sprintf("%s: already exists", conv.ID)) + continue + } + + rule := schema.UserRule{ + ID: conv.ID, + Say: conv.Say, + Category: conv.Category, + Languages: conv.Languages, + Severity: conv.Severity, + Autofix: conv.Autofix, + Message: conv.Message, + Example: conv.Example, + Include: conv.Include, + Exclude: conv.Exclude, + } + + userPolicy.Rules = append(userPolicy.Rules, rule) + addedRules = append(addedRules, rule) + existingIDs[conv.ID] = true + succeeded = append(succeeded, conv.ID) + } + + // Update defaults.languages with new languages from rules + if len(addedRules) > 0 { + policy.UpdateDefaultsLanguages(userPolicy, addedRules) + } + + // Save policy if any succeeded + if len(succeeded) > 0 { + if err := policy.SavePolicy(userPolicy, ""); err != nil { + return fmt.Errorf("failed to save policy: %w", err) + } + } + + // Print results + printConventionBatchResult("Added", succeeded, failed) + return nil +} + +func runConventionEdit(cmd *cobra.Command, args []string) error { + fileFlag, _ := cmd.Flags().GetString("file") + + // Load policy + userPolicy, err := policy.LoadPolicy("") + if err != nil { + return fmt.Errorf("failed to load policy: %w", err) + } + + // Build rule index map + ruleIndex := make(map[string]int) + for i, rule := range userPolicy.Rules { + ruleIndex[rule.ID] = i + } + + var edits []ConventionEditItem + + if fileFlag != "" { + // Batch mode: load from JSON file + data, err := os.ReadFile(fileFlag) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + if err := json.Unmarshal(data, &edits); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + if len(edits) == 0 { + return fmt.Errorf("no edits found in file") + } + } else { + // Single mode: use args + if len(args) != 1 { + return fmt.Errorf("usage: sym convention edit [--flags] or sym convention edit -f ") + } + + newID, _ := cmd.Flags().GetString("id") + say, _ := cmd.Flags().GetString("say") + category, _ := cmd.Flags().GetString("category") + languages, _ := cmd.Flags().GetStringSlice("languages") + severity, _ := cmd.Flags().GetString("severity") + message, _ := cmd.Flags().GetString("message") + example, _ := cmd.Flags().GetString("example") + include, _ := cmd.Flags().GetStringSlice("include") + exclude, _ := cmd.Flags().GetStringSlice("exclude") + + // Check if any edit flags were provided + hasChanges := newID != "" || say != "" || category != "" || len(languages) > 0 || + severity != "" || message != "" || example != "" || len(include) > 0 || len(exclude) > 0 || + cmd.Flags().Changed("autofix") + + if !hasChanges { + return fmt.Errorf("at least one edit flag must be provided (--id, --say, --category, etc.)") + } + + var autofix *bool + if cmd.Flags().Changed("autofix") { + val, _ := cmd.Flags().GetBool("autofix") + autofix = &val + } + + edits = []ConventionEditItem{{ + ID: args[0], + NewID: newID, + Say: say, + Category: category, + Languages: languages, + Severity: severity, + Autofix: autofix, + Message: message, + Example: example, + Include: include, + Exclude: exclude, + }} + } + + var succeeded []string + var failed []string + var editedRules []schema.UserRule + + // Process each edit + for _, edit := range edits { + if edit.ID == "" { + failed = append(failed, "(empty): ID is required") + continue + } + + // Check if at least one field to edit + hasEdit := edit.NewID != "" || edit.Say != "" || edit.Category != "" || + len(edit.Languages) > 0 || edit.Severity != "" || edit.Autofix != nil || + edit.Message != "" || edit.Example != "" || len(edit.Include) > 0 || len(edit.Exclude) > 0 + + if !hasEdit { + failed = append(failed, fmt.Sprintf("%s: at least one field to edit is required", edit.ID)) + continue + } + + idx, exists := ruleIndex[edit.ID] + if !exists { + failed = append(failed, fmt.Sprintf("%s: not found", edit.ID)) + continue + } + + resultText := edit.ID + + // If renaming ID + if edit.NewID != "" && edit.NewID != edit.ID { + if _, dupExists := ruleIndex[edit.NewID]; dupExists { + failed = append(failed, fmt.Sprintf("%s: '%s' already exists", edit.ID, edit.NewID)) + continue + } + + delete(ruleIndex, edit.ID) + ruleIndex[edit.NewID] = idx + userPolicy.Rules[idx].ID = edit.NewID + resultText = fmt.Sprintf("%s -> %s", edit.ID, edit.NewID) + } + + // Update other fields + if edit.Say != "" { + userPolicy.Rules[idx].Say = edit.Say + } + if edit.Category != "" { + userPolicy.Rules[idx].Category = edit.Category + } + if len(edit.Languages) > 0 { + userPolicy.Rules[idx].Languages = edit.Languages + } + if edit.Severity != "" { + userPolicy.Rules[idx].Severity = edit.Severity + } + if edit.Autofix != nil { + userPolicy.Rules[idx].Autofix = *edit.Autofix + } + if edit.Message != "" { + userPolicy.Rules[idx].Message = edit.Message + } + if edit.Example != "" { + userPolicy.Rules[idx].Example = edit.Example + } + if len(edit.Include) > 0 { + userPolicy.Rules[idx].Include = edit.Include + } + if len(edit.Exclude) > 0 { + userPolicy.Rules[idx].Exclude = edit.Exclude + } + + editedRules = append(editedRules, userPolicy.Rules[idx]) + succeeded = append(succeeded, resultText) + } + + // Update defaults.languages with new languages from edited rules + if len(editedRules) > 0 { + policy.UpdateDefaultsLanguages(userPolicy, editedRules) + } + + // Save policy if any succeeded + if len(succeeded) > 0 { + if err := policy.SavePolicy(userPolicy, ""); err != nil { + return fmt.Errorf("failed to save policy: %w", err) + } + } + + // Print results + printConventionBatchResult("Updated", succeeded, failed) + return nil +} + +func runConventionRemove(cmd *cobra.Command, args []string) error { + fileFlag, _ := cmd.Flags().GetString("file") + + // Load policy + userPolicy, err := policy.LoadPolicy("") + if err != nil { + return fmt.Errorf("failed to load policy: %w", err) + } + + // Build rule index map + ruleIndex := make(map[string]int) + for i, rule := range userPolicy.Rules { + ruleIndex[rule.ID] = i + } + + var ids []string + + if fileFlag != "" { + // Batch mode: load from JSON file + data, err := os.ReadFile(fileFlag) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + if err := json.Unmarshal(data, &ids); err != nil { + return fmt.Errorf("failed to parse JSON: %w", err) + } + if len(ids) == 0 { + return fmt.Errorf("no convention IDs found in file") + } + } else if len(args) > 0 { + // Single or batch mode: use args + ids = args + } else { + return fmt.Errorf("usage: sym convention remove [ids...] or sym convention remove -f ") + } + + var succeeded []string + var failed []string + toRemove := make(map[int]bool) + + // Process each ID + for _, id := range ids { + if id == "" { + failed = append(failed, "(empty): ID is required") + continue + } + + idx, exists := ruleIndex[id] + if !exists { + failed = append(failed, fmt.Sprintf("%s: not found", id)) + continue + } + + toRemove[idx] = true + succeeded = append(succeeded, id) + } + + // Remove rules + if len(toRemove) > 0 { + newRules := make([]schema.UserRule, 0, len(userPolicy.Rules)-len(toRemove)) + for i, rule := range userPolicy.Rules { + if !toRemove[i] { + newRules = append(newRules, rule) + } + } + userPolicy.Rules = newRules + + if err := policy.SavePolicy(userPolicy, ""); err != nil { + return fmt.Errorf("failed to save policy: %w", err) + } + } + + // Print results + printConventionBatchResult("Removed", succeeded, failed) + return nil +} + +// printConventionBatchResult prints the result of a batch operation for conventions. +func printConventionBatchResult(action string, succeeded, failed []string) { + if len(failed) == 0 && len(succeeded) > 0 { + if len(succeeded) == 1 { + printDone(fmt.Sprintf("%s convention: %s", action, succeeded[0])) + } else { + printDone(fmt.Sprintf("%s %d conventions:", action, len(succeeded))) + for _, id := range succeeded { + fmt.Printf(" • %s\n", id) + } + } + } else if len(succeeded) == 0 && len(failed) > 0 { + printWarn(fmt.Sprintf("Failed to %s any conventions:", action)) + for _, f := range failed { + fmt.Printf(" ✗ %s\n", f) + } + } else if len(succeeded) > 0 && len(failed) > 0 { + printWarn("Batch operation completed with errors:") + fmt.Printf(" ✓ %s (%d):\n", action, len(succeeded)) + for _, id := range succeeded { + fmt.Printf(" • %s\n", id) + } + fmt.Printf(" ✗ Failed (%d):\n", len(failed)) + for _, f := range failed { + fmt.Printf(" • %s\n", f) + } + } else { + printWarn("No conventions to process") + } +} diff --git a/internal/mcp/README.md b/internal/mcp/README.md index 8e7b2df..3116325 100644 --- a/internal/mcp/README.md +++ b/internal/mcp/README.md @@ -10,7 +10,7 @@ AI 코딩 도구(Claude Code, Cursor 등)와 stdio를 통해 통신하며, ``` mcp/ ├── server.go # MCP 서버 구현 (NewServer, Start, 도구 핸들러) -├── server_test.go # query_conventions 테스트 +├── server_test.go # list_convention 테스트 └── README.md ``` @@ -51,7 +51,7 @@ mcp/ |------|------|------| | `Server` | server.go:78 | MCP 서버 인스턴스 | | `RPCError` | server.go:184 | JSON-RPC 에러 타입 | -| `QueryConventionsInput` | server.go:190 | query_conventions 입력 스키마 | +| `ListConventionInput` | server.go:190 | list_convention 입력 스키마 | | `ValidateCodeInput` | server.go:196 | validate_code 입력 스키마 | | `ListCategoryInput` | server.go:201 | list_category 입력 스키마 | | `CategoryItem` | server.go:206 | 카테고리 항목 (배치용) | @@ -60,6 +60,11 @@ mcp/ | `EditCategoryInput` | server.go:230 | edit_category 입력 스키마 | | `RemoveCategoryInput` | server.go:235 | remove_category 입력 스키마 | | `ImportConventionsInput` | server.go:240 | import_convention 입력 스키마 | +| `ConventionInput` | server.go:246 | 컨벤션 항목 (배치용) | +| `ConventionEditInput` | server.go:260 | 컨벤션 편집 항목 (배치용) | +| `AddConventionInput` | server.go:275 | add_convention 입력 스키마 | +| `EditConventionInput` | server.go:280 | edit_convention 입력 스키마 | +| `RemoveConventionInput` | server.go:285 | remove_convention 입력 스키마 | | `QueryConventionsRequest` | server.go:330 | 컨벤션 조회 요청 | | `ConventionItem` | server.go:250 | 컨벤션 항목 | | `ValidateCodeRequest` | server.go:411 | 검증 요청 | @@ -85,7 +90,10 @@ mcp/ | 함수/메서드 | 설명 | |-------------|------| | `runStdioWithSDK(ctx)` | MCP SDK로 stdio 서버 실행 | -| `handleQueryConventions(params)` | 컨벤션 조회 핸들러 | +| `handleListConvention(params)` | 컨벤션 목록 핸들러 | +| `handleAddConvention(input)` | 컨벤션 추가 핸들러 | +| `handleEditConvention(input)` | 컨벤션 편집 핸들러 | +| `handleRemoveConvention(input)` | 컨벤션 삭제 핸들러 | | `filterConventions(req)` | 컨벤션 필터링 | | `isRuleRelevant(rule, req)` | 규칙 관련성 확인 | | `handleValidateCode(ctx, session, params)` | 코드 검증 핸들러 | @@ -105,5 +113,5 @@ mcp/ ## 참고 문헌 -- [MCP 도구 스키마](../../docs/COMMAND.md#mcp-도구-스키마) - query_conventions, validate_code, list_category, add_category, edit_category, remove_category, import_convention 입력/출력 스펙 +- [MCP 도구 스키마](../../docs/COMMAND.md#mcp-도구-스키마) - list_convention, validate_code, list_category, add_category, edit_category, remove_category, import_convention, add_convention, edit_convention, remove_convention 입력/출력 스펙 - [MCP 통합 가이드](../../docs/COMMAND.md#mcp-통합) - 지원 도구 및 등록 방법 diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 4a95169..138f813 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -175,7 +175,7 @@ func (s *Server) Start() error { } fmt.Fprintln(os.Stderr, "Symphony MCP server started (stdio mode)") - fmt.Fprintln(os.Stderr, "Available tools: query_conventions, validate_code, list_category") + fmt.Fprintln(os.Stderr, "Available tools: list_convention, add_convention, edit_convention, remove_convention, validate_code, list_category, add_category, edit_category, remove_category, import_convention") // Use official MCP go-sdk for stdio to ensure spec-compliant framing and lifecycle return s.runStdioWithSDK(context.Background()) @@ -187,8 +187,8 @@ type RPCError struct { Message string } -// QueryConventionsInput represents the input schema for the query_conventions tool (go-sdk). -type QueryConventionsInput struct { +// ListConventionInput represents the input schema for the list_convention tool (go-sdk). +type ListConventionInput struct { Category string `json:"category,omitempty" jsonschema:"Filter by category (optional). Use 'all' or leave empty to fetch all categories. Options: security, style, documentation, error_handling, architecture, performance, testing"` Languages []string `json:"languages,omitempty" jsonschema:"Programming languages to filter by (optional). Leave empty to get conventions for all languages. Examples: go, javascript, typescript, python, java"` } @@ -243,6 +243,50 @@ type ImportConventionsInput struct { Mode string `json:"mode,omitempty" jsonschema:"Import mode: 'append' (default) keeps existing, 'clear' removes existing first"` } +// ConventionInput represents a single convention for add operations. +type ConventionInput struct { + ID string `json:"id" jsonschema:"Rule ID (required)"` + Say string `json:"say" jsonschema:"Rule description in natural language (required)"` + Category string `json:"category,omitempty" jsonschema:"Category name"` + Languages []string `json:"languages,omitempty" jsonschema:"Programming languages"` + Severity string `json:"severity,omitempty" jsonschema:"error, warning, or info"` + Autofix bool `json:"autofix,omitempty" jsonschema:"Enable auto-fix"` + Message string `json:"message,omitempty" jsonschema:"Message to display on violation"` + Example string `json:"example,omitempty" jsonschema:"Code example"` + Include []string `json:"include,omitempty" jsonschema:"File patterns to include"` + Exclude []string `json:"exclude,omitempty" jsonschema:"File patterns to exclude"` +} + +// ConventionEditInput represents a single convention edit for batch operations. +type ConventionEditInput struct { + ID string `json:"id" jsonschema:"Current rule ID (required)"` + NewID string `json:"new_id,omitempty" jsonschema:"New rule ID"` + Say string `json:"say,omitempty" jsonschema:"New description"` + Category string `json:"category,omitempty" jsonschema:"New category name"` + Languages []string `json:"languages,omitempty" jsonschema:"New programming languages"` + Severity string `json:"severity,omitempty" jsonschema:"New severity level"` + Autofix *bool `json:"autofix,omitempty" jsonschema:"Enable auto-fix"` + Message string `json:"message,omitempty" jsonschema:"New message"` + Example string `json:"example,omitempty" jsonschema:"New code example"` + Include []string `json:"include,omitempty" jsonschema:"New file patterns to include"` + Exclude []string `json:"exclude,omitempty" jsonschema:"New file patterns to exclude"` +} + +// AddConventionInput represents the input schema for the add_convention tool (batch mode). +type AddConventionInput struct { + Conventions []ConventionInput `json:"conventions" jsonschema:"Array of conventions to add"` +} + +// EditConventionInput represents the input schema for the edit_convention tool (batch mode). +type EditConventionInput struct { + Edits []ConventionEditInput `json:"edits" jsonschema:"Array of convention edits"` +} + +// RemoveConventionInput represents the input schema for the remove_convention tool (batch mode). +type RemoveConventionInput struct { + IDs []string `json:"ids" jsonschema:"Array of convention IDs to remove"` +} + // runStdioWithSDK runs a spec-compliant MCP server over stdio using the official go-sdk. func (s *Server) runStdioWithSDK(ctx context.Context) error { server := sdkmcp.NewServer(&sdkmcp.Implementation{ @@ -250,16 +294,16 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { Version: "1.0.0", }, nil) - // Tool: query_conventions + // Tool: list_convention sdkmcp.AddTool(server, &sdkmcp.Tool{ - Name: "query_conventions", - Description: "[MANDATORY BEFORE CODING] Query project conventions BEFORE writing any code to ensure compliance from the start.", - }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input QueryConventionsInput) (*sdkmcp.CallToolResult, map[string]any, error) { + Name: "list_convention", + Description: "[MANDATORY BEFORE CODING] List project conventions BEFORE writing any code to ensure compliance from the start. Filter by category or languages.", + }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input ListConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { params := map[string]any{ "category": input.Category, "languages": input.Languages, } - result, rpcErr := s.handleQueryConventions(params) + result, rpcErr := s.handleListConvention(params) if rpcErr != nil { return &sdkmcp.CallToolResult{IsError: true}, nil, fmt.Errorf("%s", rpcErr.Message) } @@ -342,6 +386,42 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { return nil, result.(map[string]any), nil }) + // Tool: add_convention (batch mode) + sdkmcp.AddTool(server, &sdkmcp.Tool{ + Name: "add_convention", + Description: "Add conventions (rules). Pass array of {id, say, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?} objects in 'conventions' field.", + }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input AddConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { + result, rpcErr := s.handleAddConvention(input) + if rpcErr != nil { + return &sdkmcp.CallToolResult{IsError: true}, nil, fmt.Errorf("%s", rpcErr.Message) + } + return nil, result.(map[string]any), nil + }) + + // Tool: edit_convention (batch mode) + sdkmcp.AddTool(server, &sdkmcp.Tool{ + Name: "edit_convention", + Description: "Edit conventions (rules). Pass array of {id, new_id?, say?, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?} objects in 'edits' field.", + }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input EditConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { + result, rpcErr := s.handleEditConvention(input) + if rpcErr != nil { + return &sdkmcp.CallToolResult{IsError: true}, nil, fmt.Errorf("%s", rpcErr.Message) + } + return nil, result.(map[string]any), nil + }) + + // Tool: remove_convention (batch mode) + sdkmcp.AddTool(server, &sdkmcp.Tool{ + Name: "remove_convention", + Description: "Remove conventions (rules). Pass array of convention IDs in 'ids' field.", + }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input RemoveConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { + result, rpcErr := s.handleRemoveConvention(input) + if rpcErr != nil { + return &sdkmcp.CallToolResult{IsError: true}, nil, fmt.Errorf("%s", rpcErr.Message) + } + return nil, result.(map[string]any), nil + }) + // Run the server over stdio until the client disconnects return server.Run(ctx, &sdkmcp.StdioTransport{}) } @@ -361,9 +441,9 @@ type ConventionItem struct { Severity string `json:"severity"` } -// handleQueryConventions handles convention query requests. +// handleListConvention handles convention list/query requests. // It finds and returns relevant conventions by category. -func (s *Server) handleQueryConventions(params map[string]interface{}) (interface{}, *RPCError) { +func (s *Server) handleListConvention(params map[string]interface{}) (interface{}, *RPCError) { if s.userPolicy == nil && s.codePolicy == nil { return map[string]interface{}{ "conventions": []ConventionItem{}, @@ -1320,3 +1400,276 @@ func (s *Server) getUserPolicyPath() string { func (s *Server) saveUserPolicy() error { return policy.SavePolicy(s.userPolicy, "") } + +// handleAddConvention handles adding conventions (batch mode). +func (s *Server) handleAddConvention(input AddConventionInput) (interface{}, *RPCError) { + // Validate input + if len(input.Conventions) == 0 { + return nil, &RPCError{Code: -32602, Message: "At least one convention is required in 'conventions' array"} + } + + // Build existing IDs map + existingIDs := make(map[string]bool) + for _, rule := range s.userPolicy.Rules { + existingIDs[rule.ID] = true + } + + var succeeded []string + var failed []FailedItem + var addedRules []schema.UserRule + + // Process each convention + for _, conv := range input.Conventions { + // Validate + if conv.ID == "" { + failed = append(failed, FailedItem{Name: "(empty)", Reason: "Convention ID is required"}) + continue + } + if conv.Say == "" { + failed = append(failed, FailedItem{Name: conv.ID, Reason: "Convention 'say' description is required"}) + continue + } + + // Check for duplicate + if existingIDs[conv.ID] { + failed = append(failed, FailedItem{Name: conv.ID, Reason: fmt.Sprintf("Convention '%s' already exists", conv.ID)}) + continue + } + + // Add convention + rule := schema.UserRule{ + ID: conv.ID, + Say: conv.Say, + Category: conv.Category, + Languages: conv.Languages, + Severity: conv.Severity, + Autofix: conv.Autofix, + Message: conv.Message, + Example: conv.Example, + Include: conv.Include, + Exclude: conv.Exclude, + } + s.userPolicy.Rules = append(s.userPolicy.Rules, rule) + addedRules = append(addedRules, rule) + existingIDs[conv.ID] = true + succeeded = append(succeeded, conv.ID) + } + + // Update defaults.languages with new languages from rules + if len(addedRules) > 0 { + policy.UpdateDefaultsLanguages(s.userPolicy, addedRules) + } + + // Save policy if any succeeded + if len(succeeded) > 0 { + if err := s.saveUserPolicy(); err != nil { + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Failed to save policy: %v", err)} + } + } + + // Build response + return s.buildConventionBatchResponse("Added", succeeded, failed), nil +} + +// handleEditConvention handles editing conventions (batch mode). +func (s *Server) handleEditConvention(input EditConventionInput) (interface{}, *RPCError) { + // Validate input + if len(input.Edits) == 0 { + return nil, &RPCError{Code: -32602, Message: "At least one edit is required in 'edits' array"} + } + + // Build rule index map + ruleIndex := make(map[string]int) + for i, rule := range s.userPolicy.Rules { + ruleIndex[rule.ID] = i + } + + var succeeded []string + var failed []FailedItem + var editedRules []schema.UserRule + + // Process each edit + for _, edit := range input.Edits { + // Validate + if edit.ID == "" { + failed = append(failed, FailedItem{Name: "(empty)", Reason: "Convention ID is required"}) + continue + } + + // Check if at least one field to edit + hasEdit := edit.NewID != "" || edit.Say != "" || edit.Category != "" || + len(edit.Languages) > 0 || edit.Severity != "" || edit.Autofix != nil || + edit.Message != "" || edit.Example != "" || len(edit.Include) > 0 || len(edit.Exclude) > 0 + + if !hasEdit { + failed = append(failed, FailedItem{Name: edit.ID, Reason: "At least one field to edit must be provided"}) + continue + } + + // Find convention + idx, exists := ruleIndex[edit.ID] + if !exists { + failed = append(failed, FailedItem{Name: edit.ID, Reason: fmt.Sprintf("Convention '%s' not found", edit.ID)}) + continue + } + + resultText := edit.ID + + // If renaming ID + if edit.NewID != "" && edit.NewID != edit.ID { + // Check for duplicate + if _, dupExists := ruleIndex[edit.NewID]; dupExists { + failed = append(failed, FailedItem{Name: edit.ID, Reason: fmt.Sprintf("Convention '%s' already exists", edit.NewID)}) + continue + } + + // Update index map + delete(ruleIndex, edit.ID) + ruleIndex[edit.NewID] = idx + + s.userPolicy.Rules[idx].ID = edit.NewID + resultText = fmt.Sprintf("%s → %s", edit.ID, edit.NewID) + } + + // Update other fields + if edit.Say != "" { + s.userPolicy.Rules[idx].Say = edit.Say + } + if edit.Category != "" { + s.userPolicy.Rules[idx].Category = edit.Category + } + if len(edit.Languages) > 0 { + s.userPolicy.Rules[idx].Languages = edit.Languages + } + if edit.Severity != "" { + s.userPolicy.Rules[idx].Severity = edit.Severity + } + if edit.Autofix != nil { + s.userPolicy.Rules[idx].Autofix = *edit.Autofix + } + if edit.Message != "" { + s.userPolicy.Rules[idx].Message = edit.Message + } + if edit.Example != "" { + s.userPolicy.Rules[idx].Example = edit.Example + } + if len(edit.Include) > 0 { + s.userPolicy.Rules[idx].Include = edit.Include + } + if len(edit.Exclude) > 0 { + s.userPolicy.Rules[idx].Exclude = edit.Exclude + } + + editedRules = append(editedRules, s.userPolicy.Rules[idx]) + succeeded = append(succeeded, resultText) + } + + // Update defaults.languages with new languages from edited rules + if len(editedRules) > 0 { + policy.UpdateDefaultsLanguages(s.userPolicy, editedRules) + } + + // Save policy if any succeeded + if len(succeeded) > 0 { + if err := s.saveUserPolicy(); err != nil { + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Failed to save policy: %v", err)} + } + } + + // Build response + return s.buildConventionBatchResponse("Updated", succeeded, failed), nil +} + +// handleRemoveConvention handles removing conventions (batch mode). +func (s *Server) handleRemoveConvention(input RemoveConventionInput) (interface{}, *RPCError) { + // Validate input + if len(input.IDs) == 0 { + return nil, &RPCError{Code: -32602, Message: "At least one convention ID is required in 'ids' array"} + } + + // Build rule index map + ruleIndex := make(map[string]int) + for i, rule := range s.userPolicy.Rules { + ruleIndex[rule.ID] = i + } + + var succeeded []string + var failed []FailedItem + toRemove := make(map[int]bool) // indices to remove + + // Process each ID + for _, id := range input.IDs { + // Validate + if id == "" { + failed = append(failed, FailedItem{Name: "(empty)", Reason: "Convention ID is required"}) + continue + } + + // Find convention + idx, exists := ruleIndex[id] + if !exists { + failed = append(failed, FailedItem{Name: id, Reason: fmt.Sprintf("Convention '%s' not found", id)}) + continue + } + + toRemove[idx] = true + succeeded = append(succeeded, id) + } + + // Remove conventions + if len(toRemove) > 0 { + newRules := make([]schema.UserRule, 0, len(s.userPolicy.Rules)-len(toRemove)) + for i, rule := range s.userPolicy.Rules { + if !toRemove[i] { + newRules = append(newRules, rule) + } + } + s.userPolicy.Rules = newRules + + if err := s.saveUserPolicy(); err != nil { + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Failed to save policy: %v", err)} + } + } + + // Build response + return s.buildConventionBatchResponse("Removed", succeeded, failed), nil +} + +// buildConventionBatchResponse builds a standardized batch operation response for conventions. +func (s *Server) buildConventionBatchResponse(action string, succeeded []string, failed []FailedItem) map[string]interface{} { + var textContent string + + if len(failed) == 0 && len(succeeded) > 0 { + // All succeeded + textContent = fmt.Sprintf("%s %d convention(s) successfully:\n", action, len(succeeded)) + for _, id := range succeeded { + textContent += fmt.Sprintf(" ✓ %s\n", id) + } + } else if len(succeeded) == 0 && len(failed) > 0 { + // All failed + textContent = fmt.Sprintf("Failed to %s any conventions:\n", strings.ToLower(action)) + for _, f := range failed { + textContent += fmt.Sprintf(" ✗ %s: %s\n", f.Name, f.Reason) + } + } else if len(succeeded) > 0 && len(failed) > 0 { + // Partial success + textContent = "Batch operation completed with errors:\n" + textContent += fmt.Sprintf(" ✓ Succeeded (%d):\n", len(succeeded)) + for _, id := range succeeded { + textContent += fmt.Sprintf(" - %s\n", id) + } + textContent += fmt.Sprintf(" ✗ Failed (%d):\n", len(failed)) + for _, f := range failed { + textContent += fmt.Sprintf(" - %s: %s\n", f.Name, f.Reason) + } + } else { + // Nothing to do + textContent = "No conventions to process." + } + + return map[string]interface{}{ + "content": []map[string]interface{}{ + {"type": "text", "text": textContent}, + }, + } +} diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 352c039..f43135c 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -77,7 +77,7 @@ func TestQueryConventions(t *testing.T) { "languages": []interface{}{"javascript"}, } - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) @@ -100,7 +100,7 @@ func TestQueryConventions(t *testing.T) { "languages": []interface{}{"javascript"}, } - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) @@ -122,7 +122,7 @@ func TestQueryConventions(t *testing.T) { "languages": []interface{}{"typescript"}, } - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) @@ -143,7 +143,7 @@ func TestQueryConventions(t *testing.T) { "languages": []interface{}{"python"}, } - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) @@ -163,7 +163,7 @@ func TestQueryConventions(t *testing.T) { "languages": []interface{}{"javascript"}, } - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) @@ -181,7 +181,7 @@ func TestQueryConventions(t *testing.T) { t.Run("empty parameters returns all conventions", func(t *testing.T) { params := map[string]interface{}{} - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) @@ -202,7 +202,7 @@ func TestQueryConventions(t *testing.T) { "category": "security", } - result, rpcErr := server.handleQueryConventions(params) + result, rpcErr := server.handleListConvention(params) require.Nil(t, rpcErr) require.NotNil(t, result) diff --git a/npm/README.md b/npm/README.md index 83641b1..3f88739 100644 --- a/npm/README.md +++ b/npm/README.md @@ -15,12 +15,16 @@ Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 - [빠른 시작](#빠른-시작) - [MCP 설정](#mcp-설정) - [사용 가능한 MCP 도구](#사용-가능한-mcp-도구) - - [`query_conventions`](#query_conventions) + - [`list_convention`](#list_convention) - [`validate_code`](#validate_code) - [`list_category`](#list_category) - [`add_category`](#add_category) - [`edit_category`](#edit_category) - [`remove_category`](#remove_category) + - [`import_convention`](#import_convention) + - [`add_convention`](#add_convention) + - [`edit_convention`](#edit_convention) + - [`remove_convention`](#remove_convention) - [컨벤션 파일](#컨벤션-파일) - [요구사항](#요구사항) - [지원 플랫폼](#지원-플랫폼) @@ -75,10 +79,10 @@ sym dashboard ## 사용 가능한 MCP 도구 -### `query_conventions` +### `list_convention` - 프로젝트 컨벤션을 조회합니다. -- 카테고리, 파일 목록, 언어 등의 파라미터는 모두 optional입니다. +- 카테고리, 언어 등의 파라미터는 모두 optional입니다. ### `validate_code` @@ -105,6 +109,29 @@ sym dashboard - 카테고리를 삭제합니다 (배치 지원). - 필수 파라미터: `names` (배열) +### `import_convention` + +- 외부 문서(텍스트, 마크다운, 코드 파일)에서 컨벤션을 추출합니다. +- LLM을 사용하여 코딩 규칙을 자동으로 인식하고 정책에 추가합니다. +- 필수 파라미터: `path` +- 선택 파라미터: `mode` (`append` 또는 `clear`, 기본값: `append`) + +### `add_convention` + +- 새 컨벤션(규칙)을 추가합니다 (배치 지원). +- 필수 파라미터: `conventions` (배열) +- 컨벤션에 포함된 언어는 자동으로 `defaults.languages`에 추가됩니다. + +### `edit_convention` + +- 기존 컨벤션을 편집합니다 (배치 지원). +- 필수 파라미터: `edits` (배열) + +### `remove_convention` + +- 컨벤션을 삭제합니다 (배치 지원). +- 필수 파라미터: `ids` (배열) + --- ## 컨벤션 파일 From ad7d41fee33a0cd2efa4bd543c33d22f40021a7f Mon Sep 17 00:00:00 2001 From: ikjeong Date: Fri, 12 Dec 2025 09:23:26 +0000 Subject: [PATCH 3/5] feat: add MCP convert tool - Add convert tool to generate linter configs from user-policy.json - Update tool descriptions to indicate convert is needed after rule changes --- internal/mcp/server.go | 122 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 138f813..a2ac284 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -175,7 +175,7 @@ func (s *Server) Start() error { } fmt.Fprintln(os.Stderr, "Symphony MCP server started (stdio mode)") - fmt.Fprintln(os.Stderr, "Available tools: list_convention, add_convention, edit_convention, remove_convention, validate_code, list_category, add_category, edit_category, remove_category, import_convention") + fmt.Fprintln(os.Stderr, "Available tools: list_convention, add_convention, edit_convention, remove_convention, validate_code, list_category, add_category, edit_category, remove_category, import_convention, convert") // Use official MCP go-sdk for stdio to ensure spec-compliant framing and lifecycle return s.runStdioWithSDK(context.Background()) @@ -287,6 +287,12 @@ type RemoveConventionInput struct { IDs []string `json:"ids" jsonschema:"Array of convention IDs to remove"` } +// ConvertPolicyInput represents the input schema for the convert tool. +type ConvertPolicyInput struct { + InputPath string `json:"input_path,omitempty" jsonschema:"Path to user-policy.json file (optional, defaults to config or .sym/user-policy.json)"` + OutputDir string `json:"output_dir,omitempty" jsonschema:"Output directory for generated configs (optional, defaults to .sym)"` +} + // runStdioWithSDK runs a spec-compliant MCP server over stdio using the official go-sdk. func (s *Server) runStdioWithSDK(ctx context.Context) error { server := sdkmcp.NewServer(&sdkmcp.Implementation{ @@ -377,7 +383,7 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { // Tool: import_convention sdkmcp.AddTool(server, &sdkmcp.Tool{ Name: "import_convention", - Description: "Import coding conventions from external documents (txt, md, code files) into user-policy.json. Uses LLM to extract categories and rules from document content.", + Description: "Import coding conventions from external documents (txt, md, code files) into user-policy.json. Uses LLM to extract categories and rules from document content. After importing, run 'convert' to generate linter configurations.", }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input ImportConventionsInput) (*sdkmcp.CallToolResult, map[string]any, error) { result, rpcErr := s.handleImportConventions(ctx, input) if rpcErr != nil { @@ -389,7 +395,7 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { // Tool: add_convention (batch mode) sdkmcp.AddTool(server, &sdkmcp.Tool{ Name: "add_convention", - Description: "Add conventions (rules). Pass array of {id, say, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?} objects in 'conventions' field.", + Description: "Add conventions (rules). Pass array of {id, say, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?} objects in 'conventions' field. After adding rules, run 'convert' to generate linter configurations.", }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input AddConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { result, rpcErr := s.handleAddConvention(input) if rpcErr != nil { @@ -401,7 +407,7 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { // Tool: edit_convention (batch mode) sdkmcp.AddTool(server, &sdkmcp.Tool{ Name: "edit_convention", - Description: "Edit conventions (rules). Pass array of {id, new_id?, say?, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?} objects in 'edits' field.", + Description: "Edit conventions (rules). Pass array of {id, new_id?, say?, category?, languages?, severity?, autofix?, message?, example?, include?, exclude?} objects in 'edits' field. After editing rules, run 'convert' to regenerate linter configurations.", }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input EditConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { result, rpcErr := s.handleEditConvention(input) if rpcErr != nil { @@ -413,7 +419,7 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { // Tool: remove_convention (batch mode) sdkmcp.AddTool(server, &sdkmcp.Tool{ Name: "remove_convention", - Description: "Remove conventions (rules). Pass array of convention IDs in 'ids' field.", + Description: "Remove conventions (rules). Pass array of convention IDs in 'ids' field. After removing rules, run 'convert' to regenerate linter configurations.", }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input RemoveConventionInput) (*sdkmcp.CallToolResult, map[string]any, error) { result, rpcErr := s.handleRemoveConvention(input) if rpcErr != nil { @@ -422,6 +428,18 @@ func (s *Server) runStdioWithSDK(ctx context.Context) error { return nil, result.(map[string]any), nil }) + // Tool: convert + sdkmcp.AddTool(server, &sdkmcp.Tool{ + Name: "convert", + Description: "Convert user-policy.json (natural language rules) into linter-specific configurations and code-policy.json. Uses LLM to route rules to appropriate linters (ESLint, Prettier, Pylint, TSC, Checkstyle, PMD, golangci-lint).", + }, func(ctx context.Context, req *sdkmcp.CallToolRequest, input ConvertPolicyInput) (*sdkmcp.CallToolResult, map[string]any, error) { + result, rpcErr := s.handleConvertPolicy(ctx, input) + if rpcErr != nil { + return &sdkmcp.CallToolResult{IsError: true}, nil, fmt.Errorf("%s", rpcErr.Message) + } + return nil, result.(map[string]any), nil + }) + // Run the server over stdio until the client disconnects return server.Run(ctx, &sdkmcp.StdioTransport{}) } @@ -1377,6 +1395,100 @@ func (s *Server) buildImportResponse(result *importer.ImportResult, importErr er } } +// handleConvertPolicy handles the convert tool. +func (s *Server) handleConvertPolicy(ctx context.Context, input ConvertPolicyInput) (interface{}, *RPCError) { + // 1. Determine input path + inputPath := input.InputPath + if inputPath == "" { + inputPath = s.getUserPolicyPath() + } + + outputDir := input.OutputDir + if outputDir == "" { + outputDir = filepath.Dir(inputPath) + } + + // 2. Load user-policy.json + data, err := os.ReadFile(inputPath) + if err != nil { + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Failed to read policy: %v", err)} + } + var userPolicy schema.UserPolicy + if err := json.Unmarshal(data, &userPolicy); err != nil { + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Failed to parse policy: %v", err)} + } + + // 3. Setup LLM provider + llmCfg := llm.LoadConfig() + llmProvider, err := llm.New(llmCfg) + if err != nil { + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Failed to create LLM provider: %v", err)} + } + defer func() { _ = llmProvider.Close() }() + + // 4. Create converter and execute + conv := converter.NewConverter(llmProvider, outputDir) + result, err := conv.Convert(ctx, &userPolicy) + if err != nil { + if result != nil { + return s.buildConvertResponse(result, err), nil + } + return nil, &RPCError{Code: -32000, Message: fmt.Sprintf("Conversion failed: %v", err)} + } + + // 5. Reload code policy after conversion + codePolicyPath := filepath.Join(outputDir, "code-policy.json") + if codePolicy, loadErr := s.loader.LoadCodePolicy(codePolicyPath); loadErr == nil { + s.codePolicy = codePolicy + } + + return s.buildConvertResponse(result, nil), nil +} + +// buildConvertResponse builds the MCP response for convert operation. +func (s *Server) buildConvertResponse(result *converter.ConvertResult, convertErr error) map[string]interface{} { + var textContent strings.Builder + + if convertErr != nil { + textContent.WriteString("Conversion completed with errors\n\n") + } else { + textContent.WriteString("✓ Conversion completed successfully\n\n") + } + + if len(result.GeneratedFiles) > 0 { + textContent.WriteString(fmt.Sprintf("Generated %d file(s):\n", len(result.GeneratedFiles))) + for _, file := range result.GeneratedFiles { + textContent.WriteString(fmt.Sprintf(" • %s\n", file)) + } + textContent.WriteString("\n") + } + + if len(result.Errors) > 0 { + textContent.WriteString(fmt.Sprintf("Errors (%d):\n", len(result.Errors))) + for linter, err := range result.Errors { + textContent.WriteString(fmt.Sprintf(" • %s: %v\n", linter, err)) + } + textContent.WriteString("\n") + } + + if len(result.Warnings) > 0 { + textContent.WriteString(fmt.Sprintf("Warnings (%d):\n", len(result.Warnings))) + for _, warning := range result.Warnings { + textContent.WriteString(fmt.Sprintf(" • %s\n", warning)) + } + } + + if convertErr != nil { + textContent.WriteString(fmt.Sprintf("\nError: %v\n", convertErr)) + } + + return map[string]interface{}{ + "content": []map[string]interface{}{ + {"type": "text", "text": textContent.String()}, + }, + } +} + // getUserPolicyPath returns the user policy file path. func (s *Server) getUserPolicyPath() string { projectCfg, _ := config.LoadProjectConfig() From 8f091b0d04df69093777b62d8154d4469a615437 Mon Sep 17 00:00:00 2001 From: ikjeong Date: Fri, 12 Dec 2025 09:50:10 +0000 Subject: [PATCH 4/5] fix: update MCP prompt for convention queries and tool usage --- internal/cmd/mcp.go | 11 ++++++++++- internal/cmd/mcp_register.go | 9 ++++++--- internal/mcp/server.go | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/cmd/mcp.go b/internal/cmd/mcp.go index 2a61666..573e699 100644 --- a/internal/cmd/mcp.go +++ b/internal/cmd/mcp.go @@ -19,7 +19,16 @@ var mcpCmd = &cobra.Command{ LLM-based coding tools can query conventions and validate code through stdio. Tools provided by MCP server: -- query_conventions: Query conventions for given context +- list_convention: List conventions (rules) with optional filters +- add_convention: Add conventions (batch supported) +- edit_convention: Edit conventions (batch supported) +- remove_convention: Remove conventions (batch supported) +- list_category: List convention categories +- add_category: Add categories (batch supported) +- edit_category: Edit categories (batch supported) +- remove_category: Remove categories (batch supported) +- import_convention: Import conventions from external documents +- convert: Convert user policy to code policy and linter configs - validate_code: Validate code compliance with conventions Communicates via stdio for integration with Claude Desktop, Claude Code, Cursor, and other MCP clients.`, diff --git a/internal/cmd/mcp_register.go b/internal/cmd/mcp_register.go index b71e8e1..9b222a0 100644 --- a/internal/cmd/mcp_register.go +++ b/internal/cmd/mcp_register.go @@ -414,9 +414,10 @@ func getClaudeCodeInstructions() string { **Check MCP Status**: Verify Symphony MCP server is active. If unavailable, warn the user and do not proceed. -**Query Conventions**: Use ` + "`mcp__symphony__query_conventions`" + ` to retrieve relevant rules. +**Query Conventions**: Use ` + "`mcp__symphony__list_convention`" + ` to retrieve relevant rules. - Select appropriate category: security, style, documentation, error_handling, architecture, performance, testing - Filter by languages as needed +**After Updating Rules/Categories**: If you add/edit/remove conventions or categories, run ` + "`mcp__symphony__convert`" + ` to regenerate derived policy and linter configs (then re-run validation if needed). ### 2. After Writing Code @@ -452,7 +453,8 @@ alwaysApply: true ### Before Code Generation 1. **Verify Symphony MCP is active** - If not available, stop and warn user -2. **Query conventions** - Use ` + "`symphony/query_conventions`" + ` with appropriate category and language +2. **Query conventions** - Use ` + "`symphony/list_convention`" + ` with appropriate category and language +3. **After updating conventions/categories** - Use ` + "`symphony/convert`" + ` to regenerate derived policy and linter configs ### After Code Generation 1. **Validate all changes** - Use ` + "`symphony/validate_code`" + ` @@ -489,9 +491,10 @@ This project uses Symphony MCP for automated code convention management. ### Before Writing Code 1. Verify Symphony MCP server is active. If not available, warn user and stop. -2. Query relevant conventions using symphony/query_conventions tool. +2. Query relevant conventions using symphony/list_convention tool. - Categories: security, style, documentation, error_handling, architecture, performance, testing - Filter by programming language +3. If you add/edit/remove conventions or categories, run symphony/convert (then validate again if needed). ### After Writing Code 1. Always validate changes using symphony/validate_code tool (validates all git changes) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index a2ac284..e1a0d77 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -1025,7 +1025,7 @@ func (s *Server) handleListCategory() (interface{}, *RPCError) { for _, cat := range category { textContent += fmt.Sprintf("• %s\n %s\n\n", cat.Name, cat.Description) } - textContent += "Use query_conventions with a specific category to get rules for that category." + textContent += "Use list_convention with a specific category to get rules for that category." } return map[string]interface{}{ From dc55bd73093d705ad43f773880b466006b74858c Mon Sep 17 00:00:00 2001 From: ikjeong Date: Fri, 12 Dec 2025 09:52:23 +0000 Subject: [PATCH 5/5] docs: update convention management documentation --- README.md | 34 ++++- docs/ARCHITECTURE.md | 1 + docs/COMMAND.md | 23 +++- docs/CONTRIBUTING.md | 3 +- docs/CONVENTION_MANAGEMENT.md | 231 ++++++++++++++++++++++++++++++++++ docs/diagrams/class.puml | 3 +- docs/diagrams/usecase.puml | 7 +- internal/mcp/README.md | 2 +- internal/server/README.md | 3 +- npm/README.md | 34 ++++- 10 files changed, 324 insertions(+), 17 deletions(-) create mode 100644 docs/CONVENTION_MANAGEMENT.md diff --git a/README.md b/README.md index 3f88739..388943b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 - [목차](#목차) - [주요 기능](#주요-기능) - [빠른 시작](#빠른-시작) + - [컨벤션 관리](#컨벤션-관리) - [MCP 설정](#mcp-설정) - [사용 가능한 MCP 도구](#사용-가능한-mcp-도구) - [`list_convention`](#list_convention) @@ -25,6 +26,7 @@ Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 - [`add_convention`](#add_convention) - [`edit_convention`](#edit_convention) - [`remove_convention`](#remove_convention) + - [`convert`](#convert) - [컨벤션 파일](#컨벤션-파일) - [요구사항](#요구사항) - [지원 플랫폼](#지원-플랫폼) @@ -50,15 +52,24 @@ npm install -g @dev-symphony/sym # 2. 프로젝트 초기화 (.sym/ 폴더 생성 + MCP 설정) sym init - -# 3. 대시보드 실행 및 컨벤션 편집 -sym dashboard - -# 4. MCP 서버를 LLM IDE 내부에서 사용 ``` --- +## 컨벤션 관리 + +컨벤션은 아래 3가지 방식으로 관리할 수 있습니다: + +- **CLI 명령어**: `sym category|convention|import|convert` +- **MCP 도구(권장)**: `list_*`, `add_*`, `edit_*`, `remove_*`, `import_convention`, `convert` +- **Dashboard**: `sym dash`로 웹에서 편집 + +권장사항: **LLM IDE(Cursor/Claude Code 등)를 사용한다면 MCP 기반 관리**를 권장합니다(조회/편집/변환/검증을 일관된 플로우로 자동화 가능). + +예시 문장: “`docs/team-standards.md`를 컨벤션에 반영해줘.” + +자세한 내용은 [`docs/CONVENTION_MANAGEMENT.md`](docs/CONVENTION_MANAGEMENT.md)를 참고하세요. + ## MCP 설정 `sym init` 명령은 MCP 서버 구성을 자동으로 설정합니다. @@ -132,6 +143,11 @@ sym dashboard - 컨벤션을 삭제합니다 (배치 지원). - 필수 파라미터: `ids` (배열) +### `convert` + +- user-policy.json(Schema A)에서 code-policy.json(Schema B) 및 린터 설정 파일을 생성/갱신합니다. +- 컨벤션/카테고리를 추가/편집/삭제한 뒤 실행하는 것을 권장합니다. + --- ## 컨벤션 파일 @@ -143,6 +159,14 @@ Symphony는 프로젝트 컨벤션을 **정책 파일(`.sym/user-policy.json`)** sym dashboard ``` +컨벤션/카테고리를 수정한 후에는 아래 명령으로 린터 설정을 갱신하세요: + +```bash +sym convert +``` + +자세한 관리 방법은 문서에서 확인할 수 있습니다: [`docs/CONVENTION_MANAGEMENT.md`](docs/CONVENTION_MANAGEMENT.md) + 예시 정책 파일: ```json diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 366b5c7..1d1ca98 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -150,6 +150,7 @@ AI 코딩 도구(Claude Code, Cursor 등)와 stdio를 통해 통신합니다. | `add_convention` | 컨벤션 추가 (배치 지원) | | `edit_convention` | 컨벤션 편집 (배치 지원) | | `remove_convention` | 컨벤션 삭제 (배치 지원) | +| `convert` | user-policy.json → code-policy.json + 린터 설정 생성/갱신 | #### HTTP Server (`internal/server`) diff --git a/docs/COMMAND.md b/docs/COMMAND.md index ace3ce2..8024d04 100644 --- a/docs/COMMAND.md +++ b/docs/COMMAND.md @@ -49,6 +49,7 @@ Symphony (`sym`)는 코드 컨벤션 관리와 RBAC(역할 기반 접근 제어) - [add\_convention](#add_convention) - [edit\_convention](#edit_convention) - [remove\_convention](#remove_convention) + - [convert](#convert) - [등록 방법](#등록-방법) - [LLM 프로바이더](#llm-프로바이더) - [지원 프로바이더](#지원-프로바이더) @@ -785,6 +786,7 @@ sym convention remove -f ids.json - `add_convention`: 컨벤션(규칙) 추가 (배치 지원) - `edit_convention`: 컨벤션(규칙) 편집 (배치 지원) - `remove_convention`: 컨벤션(규칙) 삭제 (배치 지원) +- `convert`: user-policy.json → code-policy.json + 린터 설정 생성/갱신 (권장: 규칙/카테고리 변경 후 실행) **통신 방식**: stdio (Claude Desktop, Claude Code, Cursor 등 MCP 클라이언트와 통합) @@ -1068,7 +1070,7 @@ Available categories (7): • testing Testing rules (coverage, test patterns, etc.) -Use query_conventions with a specific category to get rules for that category. +Use list_convention with a specific category to get rules for that category. ``` #### add_category @@ -1234,6 +1236,25 @@ Rules added (3): } ``` +#### convert + +user-policy.json(Schema A)에서 code-policy.json(Schema B) 및 린터 설정 파일을 생성/갱신합니다. + +**입력 스키마**: + +| 파라미터 | 타입 | 필수 | 설명 | +|----------|------|------|------| +| `input_path` | string | 아니오 | user-policy.json 경로 (기본값: config 또는 `.sym/user-policy.json`) | +| `output_dir` | string | 아니오 | 출력 디렉토리 (기본값: `.sym`) | + +**예시**: +```json +{ + "input_path": ".sym/user-policy.json", + "output_dir": ".sym" +} +``` + ### 등록 방법 ```bash diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 9ef24c5..39ce081 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -138,8 +138,9 @@ sym-cli/ │ └── schema/ # 정책 스키마 타입 정의 (UserPolicy, CodePolicy) │ ├── docs/ # 문서 +│ ├── ARCHITECTURE.md # 아키텍처 및 패키지 의존성 │ ├── COMMAND.md # CLI 명령 참조 -│ ├── server-api.md # REST API 문서 +│ ├── CONVENTION_MANAGEMENT.md # 컨벤션/카테고리 관리 가이드 (MCP/Dashboard) │ └── CONTRIBUTING.md # 이 파일 │ └── tests/ # 테스트 픽스처 및 통합 테스트 diff --git a/docs/CONVENTION_MANAGEMENT.md b/docs/CONVENTION_MANAGEMENT.md new file mode 100644 index 0000000..496f00b --- /dev/null +++ b/docs/CONVENTION_MANAGEMENT.md @@ -0,0 +1,231 @@ +# 컨벤션/카테고리 관리 가이드 + +이 문서는 Symphony에서 **컨벤션(규칙)** 및 **카테고리**를 관리하는 방법을 정리합니다. + +- **MCP 방식**: Cursor/Claude Code 등 LLM 도구가 MCP 도구를 호출해 규칙/카테고리를 조회·변경 +- **Dashboard 방식**: 로컬 웹 대시보드에서 직접 편집 +- **Import 방식**: 기존 문서(마크다운/텍스트/코드)에서 LLM이 컨벤션을 추출해 정책에 병합 + +> 중요: 컨벤션/카테고리를 추가/편집/삭제한 뒤에는 **변환(`convert`)을 실행해** `.sym/code-policy.json` 및 린터 설정 파일을 갱신하는 것을 권장합니다. + +--- + +## 공통 개념 + +- **원본 정책(A Schema)**: `.sym/user-policy.json` + - 사람이 읽고 편집하기 쉬운 정책(카테고리/규칙) +- **파생 정책(B Schema)**: `.sym/code-policy.json` + - 검증/린팅에 쓰이는 변환 결과 +- **파생 산출물**: `.sym/.eslintrc.json`, `.sym/.prettierrc.json`, `.sym/.pylintrc` 등 + +--- + +## 1) MCP를 사용한 컨벤션/카테고리 관리 + +### 준비 + +- 프로젝트에 Symphony가 초기화돼 있어야 합니다. + +```bash +sym init +``` + +- MCP 클라이언트(Cursor/Claude Code 등)에 Symphony MCP 서버가 등록돼 있어야 합니다. + - 이 저장소는 프로젝트 루트의 `.mcp.json`에 예시를 둡니다. + +### 사용 가능한 MCP 도구 + +- **조회** + - `list_category`: 카테고리 목록 조회 + - `list_convention`: 규칙(컨벤션) 목록 조회 +- **카테고리 변경** + - `add_category`, `edit_category`, `remove_category` +- **컨벤션 변경** + - `add_convention`, `edit_convention`, `remove_convention` +- **변환** + - `convert`: user-policy.json → code-policy.json + 린터 설정 생성/갱신 + +### 기본 흐름(권장) + +1. `list_category`로 카테고리 확인 +2. 필요 시 `add_category`/`edit_category`로 카테고리 정리 +3. `list_convention`으로 규칙 확인 +4. 필요 시 `add_convention`/`edit_convention`/`remove_convention`으로 규칙 수정 +5. **`convert` 실행**(파생 정책/린터 설정 갱신) +6. (선택) `validate_code`로 Git 변경사항 검증 + +### 예시 Payload + +아래 JSON은 MCP 도구 호출 시 전달하는 입력 예시입니다. + +#### 카테고리 추가(`add_category`) + +```json +{ + "categories": [ + {"name": "accessibility", "description": "Accessibility rules (WCAG, ARIA, etc.)"} + ] +} +``` + +#### 카테고리 편집(`edit_category`) + +```json +{ + "edits": [ + {"name": "security", "description": "Security rules (updated)"}, + {"name": "style", "new_name": "formatting"} + ] +} +``` + +#### 카테고리 삭제(`remove_category`) + +```json +{ + "names": ["deprecated-category"] +} +``` + +> 참고: 규칙이 참조 중인 카테고리는 삭제할 수 없습니다(먼저 규칙의 카테고리를 변경하거나 규칙을 삭제해야 함). + +#### 컨벤션 조회(`list_convention`) + +```json +{ + "category": "security", + "languages": ["go", "python"] +} +``` + +#### 컨벤션 추가(`add_convention`) + +```json +{ + "conventions": [ + { + "id": "SEC-001", + "say": "Use parameterized queries for database operations", + "category": "security", + "languages": ["go", "python"], + "severity": "error" + } + ] +} +``` + +#### 컨벤션 편집(`edit_convention`) + +```json +{ + "edits": [ + {"id": "SEC-001", "severity": "warning"}, + {"id": "SEC-001", "say": "Use parameterized queries (prepared statements)"} + ] +} +``` + +#### 컨벤션 삭제(`remove_convention`) + +```json +{ + "ids": ["SEC-001"] +} +``` + +#### 변환(`convert`) + +```json +{ + "input_path": ".sym/user-policy.json", + "output_dir": ".sym" +} +``` + +--- + +## 2) Dashboard를 사용한 컨벤션/카테고리 관리 + +### 대시보드 실행 + +```bash +# 기본 포트 8787 +sym dashboard + +# 또는 별칭 +sym dash +``` + +이 저장소에서 빌드된 바이너리를 사용한다면 다음도 가능합니다: + +```bash +./bin/sym dash +``` + +브라우저에서 `http://localhost:8787`로 접속합니다. + +### 카테고리 관리 + +- **추가**: 새 카테고리 이름/설명을 입력해 생성 +- **편집**: 이름/설명 변경 +- **삭제**: 해당 카테고리를 참조하는 규칙이 있으면 삭제 불가 + +### 컨벤션(규칙) 관리 + +- **추가**: 규칙 ID, 설명(say), 카테고리, 언어, 심각도 등을 입력 +- **편집**: 기존 규칙의 속성 수정(언어/카테고리 변경 포함) +- **삭제**: 규칙 삭제 + +### 변환 + +대시보드에서 `user-policy.json`을 수정한 뒤, 저장을 누르면 convert할지 요청을 합니다. + +--- + +## 3) Import로 컨벤션 관리 (문서 → 정책 병합) + +Import는 팀 가이드/보안 규칙/코딩 표준 문서처럼 **이미 존재하는 문서**를 기준으로, LLM이 컨벤션을 추출해 `.sym/user-policy.json`에 병합하는 방식입니다. + +### 권장 흐름(사용자 관점) + +1. 문서 준비: 예) `docs/team-standards.md` +2. Import 실행(아래 3가지 방법 중 택1) +3. `convert` 실행(파생 정책/린터 설정 갱신) +4. (선택) `validate` 또는 MCP `validate_code`로 Git 변경사항 검증 + +### (A) CLI로 Import (`sym import`) + +```bash +# 기본: append (기존 유지 + 새 항목 추가) +sym import docs/team-standards.md + +# 기존 컨벤션을 비우고 새로 구성하려면 (주의) +sym import docs/team-standards.md --mode clear + +# Import 이후 파생 정책/린터 설정 갱신 +sym convert +``` + +### (B) MCP로 Import (`import_convention`) + +LLM IDE(Cursor/Claude Code 등)에서 Symphony MCP 도구를 사용할 수 있다면 다음 흐름을 권장합니다: + +1. `import_convention`으로 문서를 정책에 반영 +2. `convert`로 파생 산출물 갱신 +3. `validate_code`로 변경사항 검증 + +#### 예시 Payload + +```json +{ + "path": "docs/team-standards.md", + "mode": "append" +} +``` + +> 예: “`docs/team-standards.md`를 컨벤션으로 반영해줘”라고 요청하면, LLM이 `import_convention` → `convert` → (필요 시) `validate_code` 순으로 실행하는 형태를 기대할 수 있습니다. + +### (C) Dashboard에서 Import + +대시보드(`sym dash` 또는 `./bin/sym dash`)에서도 Import UI를 통해 문서를 선택해 정책에 병합할 수 있습니다. +Import 후에는 저장을 누르면 convert할지 요청을 합니다. diff --git a/docs/diagrams/class.puml b/docs/diagrams/class.puml index e07c9ee..1eadbfa 100644 --- a/docs/diagrams/class.puml +++ b/docs/diagrams/class.puml @@ -119,7 +119,8 @@ package "Validator (Execution Unit Pattern)" { package "MCP Server" { class MCPServer { - +query_conventions(category, languages): string + +list_convention(category, languages): string + +convert(): ConvertResult +validate_code(role): ValidationResult } diff --git a/docs/diagrams/usecase.puml b/docs/diagrams/usecase.puml index 06c1a5b..87aa7c6 100644 --- a/docs/diagrams/usecase.puml +++ b/docs/diagrams/usecase.puml @@ -22,8 +22,9 @@ rectangle Symphony { usecase "Validate Code\n(sym validate)" as UC_VALIDATE ' MCP Integration - usecase "Query Conventions\n(MCP Tool)" as UC_QUERY + usecase "List Conventions\n(MCP Tool)" as UC_QUERY usecase "Validate Changes\n(MCP Tool)" as UC_MCP_VALIDATE + usecase "Convert Policy\n(MCP Tool)" as UC_MCP_CONVERT ' RBAC usecase "Check Permissions\n(RBAC)" as UC_RBAC @@ -35,14 +36,16 @@ dev --> UC_DEFINE dev --> UC_VALIDATE ' AI IDE interactions (MCP protocol) -ai --> UC_QUERY : "query_conventions" +ai --> UC_QUERY : "list_convention" ai --> UC_MCP_VALIDATE : "validate_code" +ai --> UC_MCP_CONVERT : "convert" ' Internal relationships UC_DEFINE ..> UC_CONVERT : <> UC_VALIDATE ..> UC_RBAC : <> UC_MCP_VALIDATE ..> UC_VALIDATE : <> UC_CONVERT ..> UC_QUERY : "enables" +UC_MCP_CONVERT ..> UC_CONVERT : <> note right of UC_CONVERT LLM transforms natural language diff --git a/internal/mcp/README.md b/internal/mcp/README.md index 3116325..3bce211 100644 --- a/internal/mcp/README.md +++ b/internal/mcp/README.md @@ -113,5 +113,5 @@ mcp/ ## 참고 문헌 -- [MCP 도구 스키마](../../docs/COMMAND.md#mcp-도구-스키마) - list_convention, validate_code, list_category, add_category, edit_category, remove_category, import_convention, add_convention, edit_convention, remove_convention 입력/출력 스펙 +- [MCP 도구 스키마](../../docs/COMMAND.md#mcp-도구-스키마) - list_convention, validate_code, list_category, add_category, edit_category, remove_category, import_convention, add_convention, edit_convention, remove_convention, convert 입력/출력 스펙 - [MCP 통합 가이드](../../docs/COMMAND.md#mcp-통합) - 지원 도구 및 등록 방법 diff --git a/internal/server/README.md b/internal/server/README.md index 769f195..bd53a28 100644 --- a/internal/server/README.md +++ b/internal/server/README.md @@ -97,4 +97,5 @@ type Server struct { ## 참고 문헌 -- [Server REST API Reference](../../docs/server-api.md) - 엔드포인트 상세 문서 (요청/응답 스키마, 권한 모델) +- [Architecture](../../docs/ARCHITECTURE.md) - HTTP 엔드포인트 및 레이어 구조 개요 +- [Command Reference](../../docs/COMMAND.md#sym-dashboard) - 대시보드 실행 방법 diff --git a/npm/README.md b/npm/README.md index 3f88739..388943b 100644 --- a/npm/README.md +++ b/npm/README.md @@ -13,6 +13,7 @@ Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 - [목차](#목차) - [주요 기능](#주요-기능) - [빠른 시작](#빠른-시작) + - [컨벤션 관리](#컨벤션-관리) - [MCP 설정](#mcp-설정) - [사용 가능한 MCP 도구](#사용-가능한-mcp-도구) - [`list_convention`](#list_convention) @@ -25,6 +26,7 @@ Symphony는 AI 개발환경(IDE, MCP 기반 LLM Tooling)을 위한 정책 기반 - [`add_convention`](#add_convention) - [`edit_convention`](#edit_convention) - [`remove_convention`](#remove_convention) + - [`convert`](#convert) - [컨벤션 파일](#컨벤션-파일) - [요구사항](#요구사항) - [지원 플랫폼](#지원-플랫폼) @@ -50,15 +52,24 @@ npm install -g @dev-symphony/sym # 2. 프로젝트 초기화 (.sym/ 폴더 생성 + MCP 설정) sym init - -# 3. 대시보드 실행 및 컨벤션 편집 -sym dashboard - -# 4. MCP 서버를 LLM IDE 내부에서 사용 ``` --- +## 컨벤션 관리 + +컨벤션은 아래 3가지 방식으로 관리할 수 있습니다: + +- **CLI 명령어**: `sym category|convention|import|convert` +- **MCP 도구(권장)**: `list_*`, `add_*`, `edit_*`, `remove_*`, `import_convention`, `convert` +- **Dashboard**: `sym dash`로 웹에서 편집 + +권장사항: **LLM IDE(Cursor/Claude Code 등)를 사용한다면 MCP 기반 관리**를 권장합니다(조회/편집/변환/검증을 일관된 플로우로 자동화 가능). + +예시 문장: “`docs/team-standards.md`를 컨벤션에 반영해줘.” + +자세한 내용은 [`docs/CONVENTION_MANAGEMENT.md`](docs/CONVENTION_MANAGEMENT.md)를 참고하세요. + ## MCP 설정 `sym init` 명령은 MCP 서버 구성을 자동으로 설정합니다. @@ -132,6 +143,11 @@ sym dashboard - 컨벤션을 삭제합니다 (배치 지원). - 필수 파라미터: `ids` (배열) +### `convert` + +- user-policy.json(Schema A)에서 code-policy.json(Schema B) 및 린터 설정 파일을 생성/갱신합니다. +- 컨벤션/카테고리를 추가/편집/삭제한 뒤 실행하는 것을 권장합니다. + --- ## 컨벤션 파일 @@ -143,6 +159,14 @@ Symphony는 프로젝트 컨벤션을 **정책 파일(`.sym/user-policy.json`)** sym dashboard ``` +컨벤션/카테고리를 수정한 후에는 아래 명령으로 린터 설정을 갱신하세요: + +```bash +sym convert +``` + +자세한 관리 방법은 문서에서 확인할 수 있습니다: [`docs/CONVENTION_MANAGEMENT.md`](docs/CONVENTION_MANAGEMENT.md) + 예시 정책 파일: ```json