Coverage for src / moai_adk / core / config / migration.py: 10.61%
66 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""Configuration migration utilities for legacy flat config structure.
3Supports migration from legacy flat config.json structure to new nested language structure.
4"""
6from typing import Any
9def migrate_config_to_nested_structure(config: dict[str, Any]) -> dict[str, Any]:
10 """Migrate legacy flat config to nested language structure.
12 This function handles the transition from legacy flat config:
13 "conversation_language": "ko"
14 "locale": "ko"
16 To new nested structure:
17 "language": {
18 "conversation_language": "ko",
19 "conversation_language_name": "한국어"
20 }
22 Args:
23 config: Configuration dictionary that may have legacy structure.
25 Returns:
26 Configuration dictionary with nested language structure.
27 """
28 # If config already has nested language structure, return as-is
29 if "language" in config and isinstance(config["language"], dict):
30 return config
32 # If config has legacy flat structure, migrate it
33 if "conversation_language" in config and "language" not in config:
34 # Extract conversation language from legacy location
35 conversation_language = config.pop("conversation_language", "en")
36 config.pop("locale", None) # Remove legacy locale field
38 # Import enhanced language configuration
39 from ..language_config import LANGUAGE_CONFIG
41 # Extract language names from enhanced config
42 language_names = {
43 code: info["native_name"] for code, info in LANGUAGE_CONFIG.items()
44 }
46 language_name = language_names.get(conversation_language, "English")
48 # Create new nested language structure
49 config["language"] = {
50 "conversation_language": conversation_language,
51 "conversation_language_name": language_name,
52 }
54 # 2. Language settings migration
55 # Old: "language": "ko"
56 # New: "language": {"conversation_language": "ko", "conversation_language_name": "Korean"}
57 if "language" in config and isinstance(config["language"], str):
58 old_lang = config["language"]
59 lang_names = {
60 "ko": "Korean",
61 "en": "English",
62 "ja": "Japanese",
63 "zh": "Chinese"
64 }
65 config["language"] = {
66 "conversation_language": old_lang,
67 "conversation_language_name": lang_names.get(old_lang, "English")
68 }
70 return config
73def get_conversation_language(config: dict[str, Any]) -> str:
74 """Get conversation language from config with fallback handling.
76 Handles both legacy flat and new nested config structures.
78 Args:
79 config: Configuration dictionary.
81 Returns:
82 Language code (e.g., "ko", "en", "ja").
83 """
84 # First, try to get from nested structure (new format)
85 language_config = config.get("language", {})
86 if isinstance(language_config, dict):
87 result = language_config.get("conversation_language")
88 if result:
89 return result
91 # Fall back to legacy flat structure
92 result = config.get("conversation_language")
93 if result:
94 return result
96 # Default to English
97 return "en"
100def get_conversation_language_name(config: dict[str, Any]) -> str:
101 """Get conversation language name from config with fallback handling.
103 Handles both legacy flat and new nested config structures.
105 Args:
106 config: Configuration dictionary.
108 Returns:
109 Language name (e.g., "한국어", "English").
110 """
111 # First, try to get from nested structure (new format)
112 language_config = config.get("language", {})
113 if isinstance(language_config, dict):
114 result = language_config.get("conversation_language_name")
115 if result:
116 return result
118 # If we have the language code, try to map it
119 language_code = get_conversation_language(config)
121 # Import enhanced language configuration
122 from ..language_config import LANGUAGE_CONFIG
124 # Extract language names from enhanced config
125 language_names = {
126 code: info["native_name"] for code, info in LANGUAGE_CONFIG.items()
127 }
128 return language_names.get(language_code, "English")
131def migrate_config_schema_v0_17_0(config: dict[str, Any]) -> dict[str, Any]:
132 """Migrate config schema for v0.16.0 → v0.17.0 (report generation feature).
134 Adds new sections:
135 - report_generation: Control automatic report generation
136 - Enhanced github: auto_delete_branches, spec_git_workflow settings
138 This function is backward-compatible and safe for existing configs.
140 Args:
141 config: Configuration dictionary (may be v0.16.0 or earlier).
143 Returns:
144 Configuration dictionary with v0.17.0 schema.
145 """
146 # 1. Add report_generation section if missing (defaults to enabled=true, auto_create=false)
147 if "report_generation" not in config:
148 config["report_generation"] = {
149 "enabled": True,
150 "auto_create": False,
151 "warn_user": True,
152 "user_choice": "Minimal",
153 "configured_at": None, # Will be set when user configures
154 "allowed_locations": [
155 ".moai/docs/",
156 ".moai/reports/",
157 ".moai/analysis/",
158 ".moai/specs/SPEC-*/",
159 ],
160 "notes": (
161 "Control automatic report generation. 'enabled': turn on/off, "
162 "'auto_create': full (true) vs minimal (false) reports. "
163 "Helps reduce token usage."
164 ),
165 }
167 # 2. Enhance github section with new fields
168 if "github" not in config:
169 config["github"] = {}
171 github_config = config["github"]
173 # Add auto_delete_branches settings if missing
174 if "auto_delete_branches" not in github_config:
175 github_config["auto_delete_branches"] = None
176 github_config["auto_delete_branches_checked"] = False
177 github_config["auto_delete_branches_rationale"] = "Not configured"
179 # Add spec_git_workflow settings if missing
180 if "spec_git_workflow" not in github_config:
181 github_config["spec_git_workflow"] = "per_spec"
182 github_config["spec_git_workflow_configured"] = False
183 github_config["spec_git_workflow_rationale"] = (
184 "Ask per SPEC (flexible, user controls each workflow)"
185 )
187 # Add notes for new fields if missing
188 if "notes_new_fields" not in github_config:
189 github_config["notes_new_fields"] = (
190 "auto_delete_branches: whether to auto-delete feature branches after merge. "
191 "spec_git_workflow: 'feature_branch' (auto), 'develop_direct' (direct), "
192 "'per_spec' (ask per SPEC)"
193 )
195 return config
198def get_report_generation_config(config: dict[str, Any]) -> dict[str, Any]:
199 """Get report generation configuration with safe defaults.
201 Args:
202 config: Configuration dictionary.
204 Returns:
205 Report generation configuration with defaults.
206 """
207 default_config = {
208 "enabled": True,
209 "auto_create": False,
210 "warn_user": True,
211 "user_choice": "Minimal",
212 "configured_at": None,
213 "allowed_locations": [
214 ".moai/docs/",
215 ".moai/reports/",
216 ".moai/analysis/",
217 ".moai/specs/SPEC-*/",
218 ],
219 }
221 report_gen = config.get("report_generation", {})
222 if isinstance(report_gen, dict):
223 # Merge with defaults to ensure all keys exist
224 return {**default_config, **report_gen}
226 return default_config
229def get_spec_git_workflow(config: dict[str, Any]) -> str:
230 """Get SPEC git workflow setting with safe default.
232 Options:
233 - 'per_spec': Ask per SPEC (flexible, user controls)
234 - 'feature_branch': Auto-create branch for each SPEC
235 - 'develop_direct': Direct commits to develop
237 Args:
238 config: Configuration dictionary.
240 Returns:
241 Workflow setting string.
242 """
243 github_config = config.get("github", {})
244 if isinstance(github_config, dict):
245 workflow = github_config.get("spec_git_workflow")
246 if workflow in ["per_spec", "feature_branch", "develop_direct"]:
247 return workflow
249 # Default: per_spec (ask user)
250 return "per_spec"