crackerjack 0.32.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 (200) 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 +64 -6
  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 +257 -218
  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 +558 -240
  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 +66 -13
  74. crackerjack/managers/test_command_builder.py +5 -17
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +109 -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 +161 -32
  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 +174 -33
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +15 -12
  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 +3 -0
  109. crackerjack/mixins/error_handling.py +145 -0
  110. crackerjack/models/config.py +21 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +176 -107
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/models/task.py +3 -0
  115. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  116. crackerjack/monitoring/metrics_collector.py +426 -0
  117. crackerjack/monitoring/regression_prevention.py +8 -8
  118. crackerjack/monitoring/websocket_server.py +643 -0
  119. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  120. crackerjack/orchestration/coverage_improvement.py +3 -3
  121. crackerjack/orchestration/execution_strategies.py +26 -6
  122. crackerjack/orchestration/test_progress_streamer.py +8 -5
  123. crackerjack/plugins/base.py +2 -2
  124. crackerjack/plugins/hooks.py +7 -0
  125. crackerjack/plugins/managers.py +11 -8
  126. crackerjack/security/__init__.py +0 -1
  127. crackerjack/security/audit.py +90 -105
  128. crackerjack/services/anomaly_detector.py +392 -0
  129. crackerjack/services/api_extractor.py +615 -0
  130. crackerjack/services/backup_service.py +2 -2
  131. crackerjack/services/bounded_status_operations.py +15 -152
  132. crackerjack/services/cache.py +127 -1
  133. crackerjack/services/changelog_automation.py +395 -0
  134. crackerjack/services/config.py +18 -11
  135. crackerjack/services/config_merge.py +30 -85
  136. crackerjack/services/config_template.py +506 -0
  137. crackerjack/services/contextual_ai_assistant.py +48 -22
  138. crackerjack/services/coverage_badge_service.py +171 -0
  139. crackerjack/services/coverage_ratchet.py +41 -17
  140. crackerjack/services/debug.py +3 -3
  141. crackerjack/services/dependency_analyzer.py +460 -0
  142. crackerjack/services/dependency_monitor.py +14 -11
  143. crackerjack/services/documentation_generator.py +491 -0
  144. crackerjack/services/documentation_service.py +675 -0
  145. crackerjack/services/enhanced_filesystem.py +6 -5
  146. crackerjack/services/enterprise_optimizer.py +865 -0
  147. crackerjack/services/error_pattern_analyzer.py +676 -0
  148. crackerjack/services/file_hasher.py +1 -1
  149. crackerjack/services/git.py +41 -45
  150. crackerjack/services/health_metrics.py +10 -8
  151. crackerjack/services/heatmap_generator.py +735 -0
  152. crackerjack/services/initialization.py +30 -33
  153. crackerjack/services/input_validator.py +5 -97
  154. crackerjack/services/intelligent_commit.py +327 -0
  155. crackerjack/services/log_manager.py +15 -12
  156. crackerjack/services/logging.py +4 -3
  157. crackerjack/services/lsp_client.py +628 -0
  158. crackerjack/services/memory_optimizer.py +409 -0
  159. crackerjack/services/metrics.py +42 -33
  160. crackerjack/services/parallel_executor.py +416 -0
  161. crackerjack/services/pattern_cache.py +1 -1
  162. crackerjack/services/pattern_detector.py +6 -6
  163. crackerjack/services/performance_benchmarks.py +250 -576
  164. crackerjack/services/performance_cache.py +382 -0
  165. crackerjack/services/performance_monitor.py +565 -0
  166. crackerjack/services/predictive_analytics.py +510 -0
  167. crackerjack/services/quality_baseline.py +234 -0
  168. crackerjack/services/quality_baseline_enhanced.py +646 -0
  169. crackerjack/services/quality_intelligence.py +785 -0
  170. crackerjack/services/regex_patterns.py +605 -524
  171. crackerjack/services/regex_utils.py +43 -123
  172. crackerjack/services/secure_path_utils.py +5 -164
  173. crackerjack/services/secure_status_formatter.py +30 -141
  174. crackerjack/services/secure_subprocess.py +11 -92
  175. crackerjack/services/security.py +61 -30
  176. crackerjack/services/security_logger.py +18 -22
  177. crackerjack/services/server_manager.py +124 -16
  178. crackerjack/services/status_authentication.py +16 -159
  179. crackerjack/services/status_security_manager.py +4 -131
  180. crackerjack/services/terminal_utils.py +0 -0
  181. crackerjack/services/thread_safe_status_collector.py +19 -125
  182. crackerjack/services/unified_config.py +21 -13
  183. crackerjack/services/validation_rate_limiter.py +5 -54
  184. crackerjack/services/version_analyzer.py +459 -0
  185. crackerjack/services/version_checker.py +1 -1
  186. crackerjack/services/websocket_resource_limiter.py +10 -144
  187. crackerjack/services/zuban_lsp_service.py +390 -0
  188. crackerjack/slash_commands/__init__.py +2 -7
  189. crackerjack/slash_commands/run.md +2 -2
  190. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  191. crackerjack/tools/validate_regex_patterns.py +19 -48
  192. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
  193. crackerjack-0.33.1.dist-info/RECORD +229 -0
  194. crackerjack/CLAUDE.md +0 -207
  195. crackerjack/RULES.md +0 -380
  196. crackerjack/py313.py +0 -234
  197. crackerjack-0.32.0.dist-info/RECORD +0 -180
  198. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  199. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  200. {crackerjack-0.32.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()
@@ -53,8 +53,9 @@ class ConfigurationService:
53
53
  )
54
54
  return False
55
55
 
56
- def get_temp_config_path(self) -> Path | None:
57
- return getattr(self, "_temp_config_path", None)
56
+ def get_temp_config_path(self) -> str | None:
57
+ path = getattr(self, "_temp_config_path", None)
58
+ return str(path) if path else None
58
59
 
59
60
  def _determine_config_mode(self, options: OptionsProtocol) -> str:
60
61
  if options.experimental_hooks:
@@ -132,16 +133,14 @@ class ConfigurationService:
132
133
 
133
134
  with config_file.open() as f:
134
135
  yaml_result = yaml.safe_load(f)
135
- config_data = (
136
- t.cast("dict[str, t.Any]", yaml_result)
137
- if isinstance(yaml_result, dict)
138
- else {}
139
- )
136
+ config_data = yaml_result if isinstance(yaml_result, dict) else {}
140
137
  repos = config_data.get("repos", [])
141
138
  if not isinstance(repos, list):
142
139
  repos = []
143
140
  hook_count = sum(
144
- 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)
145
144
  )
146
145
  stat = config_file.stat()
147
146
 
@@ -149,7 +148,13 @@ class ConfigurationService:
149
148
  "exists": True,
150
149
  "file_size": stat.st_size,
151
150
  "modified_time": stat.st_mtime,
152
- "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
+ ),
153
158
  "hook_count": hook_count,
154
159
  "repos": [
155
160
  {
@@ -157,7 +162,7 @@ class ConfigurationService:
157
162
  "rev": repo.get("rev", "unknown"),
158
163
  "hooks": len(repo.get("hooks", [])),
159
164
  }
160
- for repo in repos
165
+ for repo in t.cast(list[dict[str, t.Any]], repos)
161
166
  if isinstance(repo, dict)
162
167
  ],
163
168
  }
@@ -301,7 +306,9 @@ class ConfigurationService:
301
306
  return {}
302
307
 
303
308
  version_updates = {}
304
- 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
+ )
305
312
  for repo in repos:
306
313
  repo_url = repo.get("repo", "")
307
314
  rev = repo.get("rev", "")