Coverage for src/jtech_installer/installer/config_generator.py: 90%

49 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-20 15:10 -0300

1"""Gerador de configuração core-config.yml personalizada.""" 

2 

3from pathlib import Path 

4from typing import Any, Dict, Optional 

5 

6import yaml 

7 

8from ..core.models import InstallationConfig, TeamType 

9 

10 

11class ConfigGenerator: 

12 """Gera configuração core-config.yml personalizada baseada no tipo de equipe.""" 

13 

14 def __init__(self, base_template_path: Optional[Path] = None): 

15 """ 

16 Inicializa o gerador de configuração. 

17 

18 Args: 

19 base_template_path: Caminho para template base do core-config.yml 

20 """ 

21 self.base_template_path = base_template_path 

22 

23 def generate_config( 

24 self, config: InstallationConfig, target_path: Path 

25 ) -> Dict[str, Any]: 

26 """ 

27 Gera configuração personalizada baseada no tipo de equipe. 

28 

29 Args: 

30 config: Configuração de instalação 

31 target_path: Caminho do projeto de destino 

32 

33 Returns: 

34 Dicionário com configuração gerada 

35 """ 

36 # Configuração base comum a todos os tipos 

37 base_config = self._get_base_config() 

38 

39 # Personalizações por tipo de equipe 

40 team_config = self._get_team_specific_config(config.team_type) 

41 

42 # Configurações específicas do projeto 

43 project_config = self._get_project_specific_config(target_path) 

44 

45 # Merge das configurações 

46 final_config = {**base_config, **team_config, **project_config} 

47 

48 return final_config 

49 

50 def _get_base_config(self) -> Dict[str, Any]: 

51 """Retorna configuração base comum a todos os tipos.""" 

52 return { 

53 "markdownExploder": True, 

54 "slashPrefix": "jtech", 

55 "qa": {"qaLocation": "docs/qa"}, 

56 "prd": { 

57 "prdFile": "docs/prd.md", 

58 "prdVersion": "v2", 

59 "prdSharded": True, 

60 "prdShardedLocation": "docs/prd", 

61 "epicFilePattern": "epic-{n}*.md", 

62 }, 

63 "architecture": { 

64 "architectureFile": "docs/architecture.md", 

65 "architectureVersion": "v2", 

66 "architectureSharded": True, 

67 "architectureShardedLocation": "docs/architecture", 

68 }, 

69 "devDebugLog": ".ai/debug-log.md", 

70 "devStoryLocation": "docs/stories", 

71 } 

72 

73 def _get_team_specific_config(self, team_type: TeamType) -> Dict[str, Any]: 

74 """Retorna configurações específicas por tipo de equipe.""" 

75 configs = { 

76 TeamType.ALL: { 

77 "customTechnicalDocuments": [ 

78 "docs/architecture/coding-standards.md", 

79 "docs/architecture/tech-stack.md", 

80 "docs/architecture/source-tree.md", 

81 "docs/architecture/deployment.md", 

82 "docs/architecture/security.md", 

83 ], 

84 "devLoadAlwaysFiles": [ 

85 "docs/architecture/coding-standards.md", 

86 "docs/architecture/tech-stack.md", 

87 "docs/architecture/source-tree.md", 

88 "docs/architecture/deployment.md", 

89 ], 

90 }, 

91 TeamType.FULLSTACK: { 

92 "customTechnicalDocuments": [ 

93 "docs/architecture/coding-standards.md", 

94 "docs/architecture/tech-stack.md", 

95 "docs/architecture/source-tree.md", 

96 "docs/architecture/deployment.md", 

97 ], 

98 "devLoadAlwaysFiles": [ 

99 "docs/architecture/coding-standards.md", 

100 "docs/architecture/tech-stack.md", 

101 "docs/architecture/source-tree.md", 

102 ], 

103 }, 

104 TeamType.NO_UI: { 

105 "customTechnicalDocuments": [ 

106 "docs/architecture/coding-standards.md", 

107 "docs/architecture/tech-stack.md", 

108 "docs/architecture/api-design.md", 

109 ], 

110 "devLoadAlwaysFiles": [ 

111 "docs/architecture/coding-standards.md", 

112 "docs/architecture/tech-stack.md", 

113 "docs/architecture/api-design.md", 

114 ], 

115 }, 

116 TeamType.IDE_MINIMAL: { 

117 "customTechnicalDocuments": None, 

118 "devLoadAlwaysFiles": [ 

119 "docs/architecture/coding-standards.md", 

120 "docs/architecture/tech-stack.md", 

121 ], 

122 }, 

123 } 

124 

125 return configs.get(team_type, {}) 

126 

127 def _get_project_specific_config( 

128 self, target_path: Path 

129 ) -> Dict[str, Any]: 

130 """Retorna configurações específicas do projeto.""" 

131 config = {} 

132 

133 # Detecta se é projeto existente (brownfield) 

134 if self._is_brownfield_project(target_path): 

135 config["projectType"] = "brownfield" 

136 

137 # Ajusta caminhos para projetos existentes 

138 if (target_path / "documentation").exists(): 

139 config["prd"]["prdFile"] = "documentation/prd.md" 

140 config["architecture"][ 

141 "architectureFile" 

142 ] = "documentation/architecture.md" 

143 config["qa"]["qaLocation"] = "documentation/qa" 

144 

145 else: 

146 config["projectType"] = "greenfield" 

147 

148 return config 

149 

150 def _is_brownfield_project(self, target_path: Path) -> bool: 

151 """Verifica se é um projeto existente (brownfield).""" 

152 indicators = [ 

153 target_path / "src", 

154 target_path / "lib", 

155 target_path / "app", 

156 target_path / "package.json", 

157 target_path / "requirements.txt", 

158 target_path / "Cargo.toml", 

159 target_path / "go.mod", 

160 ] 

161 

162 return any(indicator.exists() for indicator in indicators) 

163 

164 def write_config( 

165 self, config_dict: Dict[str, Any], target_path: Path 

166 ) -> Path: 

167 """ 

168 Escreve a configuração no arquivo core-config.yml. 

169 

170 Args: 

171 config_dict: Dicionário com configuração 

172 target_path: Caminho do projeto de destino 

173 

174 Returns: 

175 Caminho do arquivo gerado 

176 """ 

177 config_file = target_path / ".jtech-core" / "core-config.yml" 

178 

179 # Garante que o diretório existe 

180 config_file.parent.mkdir(parents=True, exist_ok=True) 

181 

182 # Escreve o arquivo YAML 

183 with open(config_file, "w", encoding="utf-8") as f: 

184 yaml.dump( 

185 config_dict, 

186 f, 

187 default_flow_style=False, 

188 allow_unicode=True, 

189 sort_keys=False, 

190 ) 

191 

192 return config_file 

193 

194 def validate_config(self, config_dict: Dict[str, Any]) -> bool: 

195 """ 

196 Valida a configuração gerada. 

197 

198 Args: 

199 config_dict: Dicionário com configuração 

200 

201 Returns: 

202 True se válida, False caso contrário 

203 """ 

204 required_keys = ["prd", "architecture", "qa", "slashPrefix"] 

205 

206 for key in required_keys: 

207 if key not in config_dict: 

208 return False 

209 

210 # Valida estrutura do PRD 

211 prd_config = config_dict.get("prd", {}) 

212 if not all(k in prd_config for k in ["prdFile", "prdVersion"]): 

213 return False 

214 

215 # Valida estrutura da arquitetura 

216 arch_config = config_dict.get("architecture", {}) 

217 if not all( 

218 k in arch_config 

219 for k in ["architectureFile", "architectureVersion"] 

220 ): 

221 return False 

222 

223 return True