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

1"""Configuration migration utilities for legacy flat config structure. 

2 

3Supports migration from legacy flat config.json structure to new nested language structure. 

4""" 

5 

6from typing import Any 

7 

8 

9def migrate_config_to_nested_structure(config: dict[str, Any]) -> dict[str, Any]: 

10 """Migrate legacy flat config to nested language structure. 

11 

12 This function handles the transition from legacy flat config: 

13 "conversation_language": "ko" 

14 "locale": "ko" 

15 

16 To new nested structure: 

17 "language": { 

18 "conversation_language": "ko", 

19 "conversation_language_name": "한국어" 

20 } 

21 

22 Args: 

23 config: Configuration dictionary that may have legacy structure. 

24 

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 

31 

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 

37 

38 # Import enhanced language configuration 

39 from ..language_config import LANGUAGE_CONFIG 

40 

41 # Extract language names from enhanced config 

42 language_names = { 

43 code: info["native_name"] for code, info in LANGUAGE_CONFIG.items() 

44 } 

45 

46 language_name = language_names.get(conversation_language, "English") 

47 

48 # Create new nested language structure 

49 config["language"] = { 

50 "conversation_language": conversation_language, 

51 "conversation_language_name": language_name, 

52 } 

53 

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 } 

69 

70 return config 

71 

72 

73def get_conversation_language(config: dict[str, Any]) -> str: 

74 """Get conversation language from config with fallback handling. 

75 

76 Handles both legacy flat and new nested config structures. 

77 

78 Args: 

79 config: Configuration dictionary. 

80 

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 

90 

91 # Fall back to legacy flat structure 

92 result = config.get("conversation_language") 

93 if result: 

94 return result 

95 

96 # Default to English 

97 return "en" 

98 

99 

100def get_conversation_language_name(config: dict[str, Any]) -> str: 

101 """Get conversation language name from config with fallback handling. 

102 

103 Handles both legacy flat and new nested config structures. 

104 

105 Args: 

106 config: Configuration dictionary. 

107 

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 

117 

118 # If we have the language code, try to map it 

119 language_code = get_conversation_language(config) 

120 

121 # Import enhanced language configuration 

122 from ..language_config import LANGUAGE_CONFIG 

123 

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") 

129 

130 

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). 

133 

134 Adds new sections: 

135 - report_generation: Control automatic report generation 

136 - Enhanced github: auto_delete_branches, spec_git_workflow settings 

137 

138 This function is backward-compatible and safe for existing configs. 

139 

140 Args: 

141 config: Configuration dictionary (may be v0.16.0 or earlier). 

142 

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 } 

166 

167 # 2. Enhance github section with new fields 

168 if "github" not in config: 

169 config["github"] = {} 

170 

171 github_config = config["github"] 

172 

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" 

178 

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 ) 

186 

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 ) 

194 

195 return config 

196 

197 

198def get_report_generation_config(config: dict[str, Any]) -> dict[str, Any]: 

199 """Get report generation configuration with safe defaults. 

200 

201 Args: 

202 config: Configuration dictionary. 

203 

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 } 

220 

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} 

225 

226 return default_config 

227 

228 

229def get_spec_git_workflow(config: dict[str, Any]) -> str: 

230 """Get SPEC git workflow setting with safe default. 

231 

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 

236 

237 Args: 

238 config: Configuration dictionary. 

239 

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 

248 

249 # Default: per_spec (ask user) 

250 return "per_spec"