crackerjack 0.33.0__py3-none-any.whl → 0.33.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (198) hide show
  1. crackerjack/__main__.py +1350 -34
  2. crackerjack/adapters/__init__.py +17 -0
  3. crackerjack/adapters/lsp_client.py +358 -0
  4. crackerjack/adapters/rust_tool_adapter.py +194 -0
  5. crackerjack/adapters/rust_tool_manager.py +193 -0
  6. crackerjack/adapters/skylos_adapter.py +231 -0
  7. crackerjack/adapters/zuban_adapter.py +560 -0
  8. crackerjack/agents/base.py +7 -3
  9. crackerjack/agents/coordinator.py +271 -33
  10. crackerjack/agents/documentation_agent.py +9 -15
  11. crackerjack/agents/dry_agent.py +3 -15
  12. crackerjack/agents/formatting_agent.py +1 -1
  13. crackerjack/agents/import_optimization_agent.py +36 -180
  14. crackerjack/agents/performance_agent.py +17 -98
  15. crackerjack/agents/performance_helpers.py +7 -31
  16. crackerjack/agents/proactive_agent.py +1 -3
  17. crackerjack/agents/refactoring_agent.py +16 -85
  18. crackerjack/agents/refactoring_helpers.py +7 -42
  19. crackerjack/agents/security_agent.py +9 -48
  20. crackerjack/agents/test_creation_agent.py +356 -513
  21. crackerjack/agents/test_specialist_agent.py +0 -4
  22. crackerjack/api.py +6 -25
  23. crackerjack/cli/cache_handlers.py +204 -0
  24. crackerjack/cli/cache_handlers_enhanced.py +683 -0
  25. crackerjack/cli/facade.py +100 -0
  26. crackerjack/cli/handlers.py +224 -9
  27. crackerjack/cli/interactive.py +6 -4
  28. crackerjack/cli/options.py +642 -55
  29. crackerjack/cli/utils.py +2 -1
  30. crackerjack/code_cleaner.py +58 -117
  31. crackerjack/config/global_lock_config.py +8 -48
  32. crackerjack/config/hooks.py +53 -62
  33. crackerjack/core/async_workflow_orchestrator.py +24 -34
  34. crackerjack/core/autofix_coordinator.py +3 -17
  35. crackerjack/core/enhanced_container.py +4 -13
  36. crackerjack/core/file_lifecycle.py +12 -89
  37. crackerjack/core/performance.py +2 -2
  38. crackerjack/core/performance_monitor.py +15 -55
  39. crackerjack/core/phase_coordinator.py +104 -204
  40. crackerjack/core/resource_manager.py +14 -90
  41. crackerjack/core/service_watchdog.py +62 -95
  42. crackerjack/core/session_coordinator.py +149 -0
  43. crackerjack/core/timeout_manager.py +14 -72
  44. crackerjack/core/websocket_lifecycle.py +13 -78
  45. crackerjack/core/workflow_orchestrator.py +171 -174
  46. crackerjack/docs/INDEX.md +11 -0
  47. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  48. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  49. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  50. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  51. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  52. crackerjack/documentation/__init__.py +31 -0
  53. crackerjack/documentation/ai_templates.py +756 -0
  54. crackerjack/documentation/dual_output_generator.py +765 -0
  55. crackerjack/documentation/mkdocs_integration.py +518 -0
  56. crackerjack/documentation/reference_generator.py +977 -0
  57. crackerjack/dynamic_config.py +55 -50
  58. crackerjack/executors/async_hook_executor.py +10 -15
  59. crackerjack/executors/cached_hook_executor.py +117 -43
  60. crackerjack/executors/hook_executor.py +8 -34
  61. crackerjack/executors/hook_lock_manager.py +26 -183
  62. crackerjack/executors/individual_hook_executor.py +13 -11
  63. crackerjack/executors/lsp_aware_hook_executor.py +270 -0
  64. crackerjack/executors/tool_proxy.py +417 -0
  65. crackerjack/hooks/lsp_hook.py +79 -0
  66. crackerjack/intelligence/adaptive_learning.py +25 -10
  67. crackerjack/intelligence/agent_orchestrator.py +2 -5
  68. crackerjack/intelligence/agent_registry.py +34 -24
  69. crackerjack/intelligence/agent_selector.py +5 -7
  70. crackerjack/interactive.py +17 -6
  71. crackerjack/managers/async_hook_manager.py +0 -1
  72. crackerjack/managers/hook_manager.py +79 -1
  73. crackerjack/managers/publish_manager.py +44 -8
  74. crackerjack/managers/test_command_builder.py +1 -15
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +98 -7
  77. crackerjack/managers/test_manager_backup.py +10 -9
  78. crackerjack/mcp/cache.py +2 -2
  79. crackerjack/mcp/client_runner.py +1 -1
  80. crackerjack/mcp/context.py +191 -68
  81. crackerjack/mcp/dashboard.py +7 -5
  82. crackerjack/mcp/enhanced_progress_monitor.py +31 -28
  83. crackerjack/mcp/file_monitor.py +30 -23
  84. crackerjack/mcp/progress_components.py +31 -21
  85. crackerjack/mcp/progress_monitor.py +50 -53
  86. crackerjack/mcp/rate_limiter.py +6 -6
  87. crackerjack/mcp/server_core.py +17 -16
  88. crackerjack/mcp/service_watchdog.py +2 -1
  89. crackerjack/mcp/state.py +4 -7
  90. crackerjack/mcp/task_manager.py +11 -9
  91. crackerjack/mcp/tools/core_tools.py +173 -32
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +8 -10
  94. crackerjack/mcp/tools/execution_tools_backup.py +42 -30
  95. crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
  96. crackerjack/mcp/tools/intelligence_tools.py +5 -2
  97. crackerjack/mcp/tools/monitoring_tools.py +33 -70
  98. crackerjack/mcp/tools/proactive_tools.py +24 -11
  99. crackerjack/mcp/tools/progress_tools.py +5 -8
  100. crackerjack/mcp/tools/utility_tools.py +20 -14
  101. crackerjack/mcp/tools/workflow_executor.py +62 -40
  102. crackerjack/mcp/websocket/app.py +8 -0
  103. crackerjack/mcp/websocket/endpoints.py +352 -357
  104. crackerjack/mcp/websocket/jobs.py +40 -57
  105. crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
  106. crackerjack/mcp/websocket/server.py +7 -25
  107. crackerjack/mcp/websocket/websocket_handler.py +6 -17
  108. crackerjack/mixins/__init__.py +0 -2
  109. crackerjack/mixins/error_handling.py +1 -70
  110. crackerjack/models/config.py +12 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +122 -122
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  115. crackerjack/monitoring/metrics_collector.py +426 -0
  116. crackerjack/monitoring/regression_prevention.py +8 -8
  117. crackerjack/monitoring/websocket_server.py +643 -0
  118. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  119. crackerjack/orchestration/coverage_improvement.py +3 -3
  120. crackerjack/orchestration/execution_strategies.py +26 -6
  121. crackerjack/orchestration/test_progress_streamer.py +8 -5
  122. crackerjack/plugins/base.py +2 -2
  123. crackerjack/plugins/hooks.py +7 -0
  124. crackerjack/plugins/managers.py +11 -8
  125. crackerjack/security/__init__.py +0 -1
  126. crackerjack/security/audit.py +6 -35
  127. crackerjack/services/anomaly_detector.py +392 -0
  128. crackerjack/services/api_extractor.py +615 -0
  129. crackerjack/services/backup_service.py +2 -2
  130. crackerjack/services/bounded_status_operations.py +15 -152
  131. crackerjack/services/cache.py +127 -1
  132. crackerjack/services/changelog_automation.py +395 -0
  133. crackerjack/services/config.py +15 -9
  134. crackerjack/services/config_merge.py +19 -80
  135. crackerjack/services/config_template.py +506 -0
  136. crackerjack/services/contextual_ai_assistant.py +48 -22
  137. crackerjack/services/coverage_badge_service.py +171 -0
  138. crackerjack/services/coverage_ratchet.py +27 -25
  139. crackerjack/services/debug.py +3 -3
  140. crackerjack/services/dependency_analyzer.py +460 -0
  141. crackerjack/services/dependency_monitor.py +14 -11
  142. crackerjack/services/documentation_generator.py +491 -0
  143. crackerjack/services/documentation_service.py +675 -0
  144. crackerjack/services/enhanced_filesystem.py +6 -5
  145. crackerjack/services/enterprise_optimizer.py +865 -0
  146. crackerjack/services/error_pattern_analyzer.py +676 -0
  147. crackerjack/services/file_hasher.py +1 -1
  148. crackerjack/services/git.py +8 -25
  149. crackerjack/services/health_metrics.py +10 -8
  150. crackerjack/services/heatmap_generator.py +735 -0
  151. crackerjack/services/initialization.py +11 -30
  152. crackerjack/services/input_validator.py +5 -97
  153. crackerjack/services/intelligent_commit.py +327 -0
  154. crackerjack/services/log_manager.py +15 -12
  155. crackerjack/services/logging.py +4 -3
  156. crackerjack/services/lsp_client.py +628 -0
  157. crackerjack/services/memory_optimizer.py +19 -87
  158. crackerjack/services/metrics.py +42 -33
  159. crackerjack/services/parallel_executor.py +9 -67
  160. crackerjack/services/pattern_cache.py +1 -1
  161. crackerjack/services/pattern_detector.py +6 -6
  162. crackerjack/services/performance_benchmarks.py +18 -59
  163. crackerjack/services/performance_cache.py +20 -81
  164. crackerjack/services/performance_monitor.py +27 -95
  165. crackerjack/services/predictive_analytics.py +510 -0
  166. crackerjack/services/quality_baseline.py +234 -0
  167. crackerjack/services/quality_baseline_enhanced.py +646 -0
  168. crackerjack/services/quality_intelligence.py +785 -0
  169. crackerjack/services/regex_patterns.py +605 -524
  170. crackerjack/services/regex_utils.py +43 -123
  171. crackerjack/services/secure_path_utils.py +5 -164
  172. crackerjack/services/secure_status_formatter.py +30 -141
  173. crackerjack/services/secure_subprocess.py +11 -92
  174. crackerjack/services/security.py +9 -41
  175. crackerjack/services/security_logger.py +12 -24
  176. crackerjack/services/server_manager.py +124 -16
  177. crackerjack/services/status_authentication.py +16 -159
  178. crackerjack/services/status_security_manager.py +4 -131
  179. crackerjack/services/thread_safe_status_collector.py +19 -125
  180. crackerjack/services/unified_config.py +21 -13
  181. crackerjack/services/validation_rate_limiter.py +5 -54
  182. crackerjack/services/version_analyzer.py +459 -0
  183. crackerjack/services/version_checker.py +1 -1
  184. crackerjack/services/websocket_resource_limiter.py +10 -144
  185. crackerjack/services/zuban_lsp_service.py +390 -0
  186. crackerjack/slash_commands/__init__.py +2 -7
  187. crackerjack/slash_commands/run.md +2 -2
  188. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  189. crackerjack/tools/validate_regex_patterns.py +19 -48
  190. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
  191. crackerjack-0.33.1.dist-info/RECORD +229 -0
  192. crackerjack/CLAUDE.md +0 -207
  193. crackerjack/RULES.md +0 -380
  194. crackerjack/py313.py +0 -234
  195. crackerjack-0.33.0.dist-info/RECORD +0 -187
  196. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  197. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  198. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,395 @@
1
+ """Automatic changelog generation and updates service."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from rich.console import Console
8
+
9
+ from .git import GitService
10
+
11
+
12
+ class ChangelogEntry:
13
+ """Represents a single changelog entry."""
14
+
15
+ def __init__(
16
+ self,
17
+ entry_type: str,
18
+ description: str,
19
+ commit_hash: str = "",
20
+ breaking_change: bool = False,
21
+ ) -> None:
22
+ self.type = entry_type
23
+ self.description = description
24
+ self.commit_hash = commit_hash
25
+ self.breaking_change = breaking_change
26
+
27
+ def to_markdown(self) -> str:
28
+ """Convert entry to markdown format."""
29
+ prefix = "**BREAKING:** " if self.breaking_change else ""
30
+ return f"- {prefix}{self.description}"
31
+
32
+
33
+ class ChangelogGenerator:
34
+ """Generate and update changelogs based on git commits."""
35
+
36
+ def __init__(self, console: Console, git_service: GitService) -> None:
37
+ self.console = console
38
+ self.git = git_service
39
+
40
+ # Conventional commit type mappings to changelog sections
41
+ self.type_mappings = {
42
+ "feat": "Added",
43
+ "fix": "Fixed",
44
+ "docs": "Documentation",
45
+ "style": "Changed",
46
+ "refactor": "Changed",
47
+ "test": "Testing",
48
+ "chore": "Internal",
49
+ "perf": "Performance",
50
+ "build": "Build",
51
+ "ci": "CI/CD",
52
+ "revert": "Reverted",
53
+ }
54
+
55
+ # Regex patterns for parsing commit messages
56
+ self.conventional_commit_pattern = re.compile( # REGEX OK: conventional commit parsing
57
+ r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s*(?P<description>.+)$"
58
+ )
59
+
60
+ self.breaking_change_pattern = (
61
+ re.compile( # REGEX OK: breaking change detection
62
+ r"BREAKING\s*CHANGE[:]\s*(.+)", re.IGNORECASE | re.MULTILINE
63
+ )
64
+ )
65
+
66
+ def parse_commit_message(
67
+ self, commit_message: str, commit_hash: str = ""
68
+ ) -> ChangelogEntry | None:
69
+ """Parse a commit message into a changelog entry."""
70
+ # Split commit message into header and body
71
+ lines = commit_message.strip().split("\n")
72
+ header = lines[0].strip()
73
+ body = "\n".join(lines[1:]).strip() if len(lines) > 1 else ""
74
+
75
+ # Try to match conventional commit format
76
+ match = self.conventional_commit_pattern.match(header)
77
+ if not match:
78
+ # Fallback for non-conventional commits
79
+ return self._parse_non_conventional_commit(header, body, commit_hash)
80
+
81
+ commit_type = match.group("type").lower()
82
+ scope = match.group("scope") or ""
83
+ breaking_marker = match.group("breaking") == "!"
84
+ description = match.group("description").strip()
85
+
86
+ # Check for breaking changes in body
87
+ breaking_in_body = bool(self.breaking_change_pattern.search(body))
88
+ breaking_change = breaking_marker or breaking_in_body
89
+
90
+ # Map commit type to changelog section
91
+ changelog_section = self.type_mappings.get(commit_type, "Changed")
92
+
93
+ # Format description
94
+ formatted_description = self._format_description(
95
+ description, scope, commit_type
96
+ )
97
+
98
+ return ChangelogEntry(
99
+ entry_type=changelog_section,
100
+ description=formatted_description,
101
+ commit_hash=commit_hash,
102
+ breaking_change=breaking_change,
103
+ )
104
+
105
+ def _parse_non_conventional_commit(
106
+ self, header: str, body: str, commit_hash: str
107
+ ) -> ChangelogEntry | None:
108
+ """Parse non-conventional commit messages."""
109
+ # Simple heuristics for non-conventional commits
110
+ header_lower = header.lower()
111
+
112
+ if any(
113
+ keyword in header_lower for keyword in ("add", "new", "create", "implement")
114
+ ):
115
+ entry_type = "Added"
116
+ elif any(
117
+ keyword in header_lower for keyword in ("fix", "bug", "resolve", "correct")
118
+ ):
119
+ entry_type = "Fixed"
120
+ elif any(
121
+ keyword in header_lower
122
+ for keyword in ("update", "change", "modify", "improve")
123
+ ):
124
+ entry_type = "Changed"
125
+ elif any(keyword in header_lower for keyword in ("remove", "delete", "drop")):
126
+ entry_type = "Removed"
127
+ elif any(keyword in header_lower for keyword in ("doc", "readme", "comment")):
128
+ entry_type = "Documentation"
129
+ else:
130
+ entry_type = "Changed"
131
+
132
+ # Check for breaking changes
133
+ breaking_change = bool(self.breaking_change_pattern.search(body))
134
+
135
+ return ChangelogEntry(
136
+ entry_type=entry_type,
137
+ description=header,
138
+ commit_hash=commit_hash,
139
+ breaking_change=breaking_change,
140
+ )
141
+
142
+ def _format_description(
143
+ self, description: str, scope: str, commit_type: str
144
+ ) -> str:
145
+ """Format the changelog description."""
146
+ # Capitalize first letter
147
+ description = description[0].upper() + description[1:] if description else ""
148
+
149
+ # Add scope context if present
150
+ if scope:
151
+ # Only add scope if it's not already mentioned in description
152
+ if scope.lower() not in description.lower():
153
+ description = f"{scope}: {description}"
154
+
155
+ return description
156
+
157
+ def generate_changelog_entries(
158
+ self, since_version: str | None = None, target_file: Path | None = None
159
+ ) -> dict[str, list[ChangelogEntry]]:
160
+ """Generate changelog entries from git commits."""
161
+ try:
162
+ # Get git commits
163
+ git_result = self._get_git_commits(since_version)
164
+ if not git_result:
165
+ return {}
166
+
167
+ # Parse commits into entries
168
+ return self._parse_commits_to_entries(git_result)
169
+
170
+ except Exception as e:
171
+ self.console.print(f"[red]❌[/red] Error generating changelog entries: {e}")
172
+ return {}
173
+
174
+ def _get_git_commits(self, since_version: str | None = None) -> str | None:
175
+ """Get git commit log output."""
176
+ # Build git command
177
+ git_command = self._build_git_log_command(since_version)
178
+
179
+ # Execute git command
180
+ result = self.git._run_git_command(git_command)
181
+ if result.returncode != 0:
182
+ self.console.print(
183
+ f"[yellow]âš ī¸[/yellow] Failed to get git log: {result.stderr}"
184
+ )
185
+ return None
186
+
187
+ return result.stdout
188
+
189
+ def _build_git_log_command(self, since_version: str | None = None) -> list[str]:
190
+ """Build the git log command based on parameters."""
191
+ if since_version:
192
+ return [
193
+ "log",
194
+ f"{since_version}..HEAD",
195
+ "--oneline",
196
+ "--no-merges",
197
+ ]
198
+ # Get commits since last release tag or last 50 commits
199
+ return ["log", "-50", "--oneline", "--no-merges"]
200
+
201
+ def _parse_commits_to_entries(
202
+ self, git_output: str
203
+ ) -> dict[str, list[ChangelogEntry]]:
204
+ """Parse git commit output into changelog entries."""
205
+ entries_by_type: dict[str, list[ChangelogEntry]] = {}
206
+
207
+ for line in git_output.strip().split("\n"):
208
+ if not line.strip():
209
+ continue
210
+
211
+ entry = self._process_commit_line(line)
212
+ if entry:
213
+ self._add_entry_to_collection(entry, entries_by_type)
214
+
215
+ return entries_by_type
216
+
217
+ def _process_commit_line(self, line: str) -> ChangelogEntry | None:
218
+ """Process a single commit line into a changelog entry."""
219
+ # Parse commit hash and message
220
+ parts = line.strip().split(" ", 1)
221
+ if len(parts) < 2:
222
+ return None
223
+
224
+ commit_hash = parts[0]
225
+ commit_message = parts[1]
226
+
227
+ # Get full commit message
228
+ full_message = self._get_full_commit_message(commit_hash, commit_message)
229
+
230
+ # Parse into changelog entry
231
+ return self.parse_commit_message(full_message, commit_hash)
232
+
233
+ def _get_full_commit_message(self, commit_hash: str, fallback_message: str) -> str:
234
+ """Get the full commit message for detailed parsing."""
235
+ full_commit_result = self.git._run_git_command(
236
+ ["show", "--format=%B", "--no-patch", commit_hash]
237
+ )
238
+
239
+ return (
240
+ full_commit_result.stdout
241
+ if full_commit_result.returncode == 0
242
+ else fallback_message
243
+ )
244
+
245
+ def _add_entry_to_collection(
246
+ self, entry: ChangelogEntry, entries_by_type: dict[str, list[ChangelogEntry]]
247
+ ) -> None:
248
+ """Add a changelog entry to the appropriate type collection."""
249
+ if entry.type not in entries_by_type:
250
+ entries_by_type[entry.type] = []
251
+ entries_by_type[entry.type].append(entry)
252
+
253
+ def update_changelog(
254
+ self,
255
+ changelog_path: Path,
256
+ new_version: str,
257
+ entries_by_type: dict[str, list[ChangelogEntry]] | None = None,
258
+ ) -> bool:
259
+ """Update the changelog file with new entries."""
260
+ try:
261
+ if entries_by_type is None:
262
+ entries_by_type = self.generate_changelog_entries()
263
+
264
+ if not entries_by_type:
265
+ self.console.print("[yellow]â„šī¸[/yellow] No new changelog entries to add")
266
+ return True
267
+
268
+ # Read existing changelog
269
+ existing_content = ""
270
+ if changelog_path.exists():
271
+ existing_content = changelog_path.read_text(encoding="utf-8")
272
+
273
+ # Generate new section
274
+ new_section = self._generate_changelog_section(new_version, entries_by_type)
275
+
276
+ # Insert new section
277
+ updated_content = self._insert_new_section(existing_content, new_section)
278
+
279
+ # Write updated changelog
280
+ changelog_path.write_text(updated_content, encoding="utf-8")
281
+
282
+ self.console.print(
283
+ f"[green]✅[/green] Updated {changelog_path.name} with {len(entries_by_type)} sections"
284
+ )
285
+ return True
286
+
287
+ except Exception as e:
288
+ self.console.print(f"[red]❌[/red] Failed to update changelog: {e}")
289
+ return False
290
+
291
+ def _generate_changelog_section(
292
+ self, version: str, entries_by_type: dict[str, list[ChangelogEntry]]
293
+ ) -> str:
294
+ """Generate a new changelog section."""
295
+ today = datetime.now().strftime("%Y-%m-%d")
296
+ section_lines = [f"## [{version}] - {today}", ""]
297
+
298
+ # Order sections by importance
299
+ section_order = [
300
+ "Added",
301
+ "Changed",
302
+ "Fixed",
303
+ "Removed",
304
+ "Performance",
305
+ "Security",
306
+ "Deprecated",
307
+ "Documentation",
308
+ "Testing",
309
+ "Build",
310
+ "CI/CD",
311
+ "Internal",
312
+ ]
313
+
314
+ for section_name in section_order:
315
+ if section_name in entries_by_type:
316
+ entries = entries_by_type[section_name]
317
+ if entries:
318
+ section_lines.extend((f"### {section_name}", ""))
319
+
320
+ # Sort entries: breaking changes first, then alphabetically
321
+ entries.sort(
322
+ key=lambda e: (not e.breaking_change, e.description.lower())
323
+ )
324
+
325
+ for entry in entries:
326
+ section_lines.append(entry.to_markdown())
327
+ section_lines.append("")
328
+
329
+ return "\n".join(section_lines)
330
+
331
+ def _insert_new_section(self, existing_content: str, new_section: str) -> str:
332
+ """Insert new section into existing changelog content."""
333
+ if not existing_content.strip():
334
+ # Create new changelog
335
+ header = """# Changelog
336
+
337
+ All notable changes to this project will be documented in this file.
338
+
339
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
340
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
341
+
342
+ """
343
+ return header + new_section
344
+
345
+ # Find where to insert (after header, before first existing version)
346
+ lines = existing_content.split("\n")
347
+ insert_index = 0
348
+
349
+ # Find the insertion point (after the header, before first version)
350
+ for i, line in enumerate(lines):
351
+ if line.strip().startswith("## ["):
352
+ insert_index = i
353
+ break
354
+ else:
355
+ # No existing version sections found, insert at end
356
+ insert_index = len(lines)
357
+
358
+ # Insert new section
359
+ new_lines = (
360
+ lines[:insert_index] + new_section.split("\n") + lines[insert_index:]
361
+ )
362
+ return "\n".join(new_lines)
363
+
364
+ def generate_changelog_from_commits(
365
+ self, changelog_path: Path, version: str, since_version: str | None = None
366
+ ) -> bool:
367
+ """Generate and update changelog from git commits."""
368
+ self.console.print(
369
+ f"[cyan]📝[/cyan] Generating changelog entries for version {version}..."
370
+ )
371
+
372
+ entries = self.generate_changelog_entries(since_version)
373
+ if not entries:
374
+ self.console.print("[yellow]â„šī¸[/yellow] No changelog entries generated")
375
+ return True
376
+
377
+ # Display preview
378
+ self._display_changelog_preview(entries)
379
+
380
+ return self.update_changelog(changelog_path, version, entries)
381
+
382
+ def _display_changelog_preview(
383
+ self, entries_by_type: dict[str, list[ChangelogEntry]]
384
+ ) -> None:
385
+ """Display a preview of generated changelog entries."""
386
+ self.console.print("[cyan]📋[/cyan] Changelog preview:")
387
+
388
+ for section_name, entries in entries_by_type.items():
389
+ if entries:
390
+ self.console.print(f"[bold]{section_name}:[/bold]")
391
+ for entry in entries[:3]: # Show first 3 entries
392
+ self.console.print(f" {entry.to_markdown()}")
393
+ if len(entries) > 3:
394
+ self.console.print(f" [dim]... and {len(entries) - 3} more[/dim]")
395
+ self.console.print()
@@ -133,16 +133,14 @@ class ConfigurationService:
133
133
 
134
134
  with config_file.open() as f:
135
135
  yaml_result = yaml.safe_load(f)
136
- config_data = (
137
- t.cast("dict[str, t.Any]", yaml_result)
138
- if isinstance(yaml_result, dict)
139
- else {}
140
- )
136
+ config_data = yaml_result if isinstance(yaml_result, dict) else {}
141
137
  repos = config_data.get("repos", [])
142
138
  if not isinstance(repos, list):
143
139
  repos = []
144
140
  hook_count = sum(
145
- len(repo.get("hooks", [])) for repo in repos if isinstance(repo, dict)
141
+ len(repo.get("hooks", []))
142
+ for repo in t.cast(list[dict[str, t.Any]], repos)
143
+ if isinstance(repo, dict)
146
144
  )
147
145
  stat = config_file.stat()
148
146
 
@@ -150,7 +148,13 @@ class ConfigurationService:
150
148
  "exists": True,
151
149
  "file_size": stat.st_size,
152
150
  "modified_time": stat.st_mtime,
153
- "repo_count": len([r for r in repos if isinstance(r, dict)]),
151
+ "repo_count": len(
152
+ [
153
+ r
154
+ for r in t.cast(list[dict[str, t.Any]], repos)
155
+ if isinstance(r, dict)
156
+ ]
157
+ ),
154
158
  "hook_count": hook_count,
155
159
  "repos": [
156
160
  {
@@ -158,7 +162,7 @@ class ConfigurationService:
158
162
  "rev": repo.get("rev", "unknown"),
159
163
  "hooks": len(repo.get("hooks", [])),
160
164
  }
161
- for repo in repos
165
+ for repo in t.cast(list[dict[str, t.Any]], repos)
162
166
  if isinstance(repo, dict)
163
167
  ],
164
168
  }
@@ -302,7 +306,9 @@ class ConfigurationService:
302
306
  return {}
303
307
 
304
308
  version_updates = {}
305
- repos = config.get("repos", []) if isinstance(config, dict) else []
309
+ repos: list[dict[str, t.Any]] = (
310
+ config.get("repos", []) if isinstance(config, dict) else []
311
+ )
306
312
  for repo in repos:
307
313
  repo_url = repo.get("repo", "")
308
314
  rev = repo.get("rev", "")