kodit 0.4.3__py3-none-any.whl → 0.5.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 kodit might be problematic. Click here for more details.

Files changed (135) hide show
  1. kodit/_version.py +2 -2
  2. kodit/app.py +51 -23
  3. kodit/application/factories/reporting_factory.py +6 -2
  4. kodit/application/factories/server_factory.py +353 -0
  5. kodit/application/services/code_search_application_service.py +144 -0
  6. kodit/application/services/commit_indexing_application_service.py +700 -0
  7. kodit/application/services/indexing_worker_service.py +13 -44
  8. kodit/application/services/queue_service.py +24 -3
  9. kodit/application/services/reporting.py +0 -2
  10. kodit/application/services/sync_scheduler.py +15 -31
  11. kodit/cli.py +2 -753
  12. kodit/cli_utils.py +2 -9
  13. kodit/config.py +4 -97
  14. kodit/database.py +38 -1
  15. kodit/domain/enrichments/__init__.py +1 -0
  16. kodit/domain/enrichments/architecture/__init__.py +1 -0
  17. kodit/domain/enrichments/architecture/architecture.py +20 -0
  18. kodit/domain/enrichments/architecture/physical/__init__.py +1 -0
  19. kodit/domain/enrichments/architecture/physical/discovery_notes.py +14 -0
  20. kodit/domain/enrichments/architecture/physical/formatter.py +11 -0
  21. kodit/domain/enrichments/architecture/physical/physical.py +17 -0
  22. kodit/domain/enrichments/development/__init__.py +1 -0
  23. kodit/domain/enrichments/development/development.py +18 -0
  24. kodit/domain/enrichments/development/snippet/__init__.py +1 -0
  25. kodit/domain/enrichments/development/snippet/snippet.py +21 -0
  26. kodit/domain/enrichments/enricher.py +17 -0
  27. kodit/domain/enrichments/enrichment.py +39 -0
  28. kodit/domain/enrichments/request.py +12 -0
  29. kodit/domain/enrichments/response.py +11 -0
  30. kodit/domain/enrichments/usage/__init__.py +1 -0
  31. kodit/domain/enrichments/usage/api_docs.py +19 -0
  32. kodit/domain/enrichments/usage/usage.py +18 -0
  33. kodit/domain/{entities.py → entities/__init__.py} +50 -195
  34. kodit/domain/entities/git.py +190 -0
  35. kodit/domain/factories/__init__.py +1 -0
  36. kodit/domain/factories/git_repo_factory.py +76 -0
  37. kodit/domain/protocols.py +264 -64
  38. kodit/domain/services/bm25_service.py +5 -1
  39. kodit/domain/services/embedding_service.py +3 -0
  40. kodit/domain/services/enrichment_service.py +9 -30
  41. kodit/domain/services/git_repository_service.py +429 -0
  42. kodit/domain/services/git_service.py +300 -0
  43. kodit/domain/services/physical_architecture_service.py +182 -0
  44. kodit/domain/services/task_status_query_service.py +2 -2
  45. kodit/domain/value_objects.py +87 -135
  46. kodit/infrastructure/api/client/__init__.py +0 -2
  47. kodit/infrastructure/api/v1/__init__.py +0 -4
  48. kodit/infrastructure/api/v1/dependencies.py +92 -46
  49. kodit/infrastructure/api/v1/routers/__init__.py +0 -6
  50. kodit/infrastructure/api/v1/routers/commits.py +352 -0
  51. kodit/infrastructure/api/v1/routers/queue.py +2 -2
  52. kodit/infrastructure/api/v1/routers/repositories.py +282 -0
  53. kodit/infrastructure/api/v1/routers/search.py +31 -14
  54. kodit/infrastructure/api/v1/schemas/__init__.py +0 -24
  55. kodit/infrastructure/api/v1/schemas/commit.py +96 -0
  56. kodit/infrastructure/api/v1/schemas/context.py +2 -0
  57. kodit/infrastructure/api/v1/schemas/enrichment.py +29 -0
  58. kodit/infrastructure/api/v1/schemas/repository.py +128 -0
  59. kodit/infrastructure/api/v1/schemas/search.py +12 -9
  60. kodit/infrastructure/api/v1/schemas/snippet.py +58 -0
  61. kodit/infrastructure/api/v1/schemas/tag.py +31 -0
  62. kodit/infrastructure/api/v1/schemas/task_status.py +2 -0
  63. kodit/infrastructure/bm25/local_bm25_repository.py +16 -4
  64. kodit/infrastructure/bm25/vectorchord_bm25_repository.py +68 -52
  65. kodit/infrastructure/cloning/git/git_python_adaptor.py +534 -0
  66. kodit/infrastructure/cloning/git/working_copy.py +1 -1
  67. kodit/infrastructure/embedding/embedding_factory.py +3 -2
  68. kodit/infrastructure/embedding/local_vector_search_repository.py +1 -1
  69. kodit/infrastructure/embedding/vectorchord_vector_search_repository.py +111 -84
  70. kodit/infrastructure/enricher/__init__.py +1 -0
  71. kodit/infrastructure/enricher/enricher_factory.py +53 -0
  72. kodit/infrastructure/{enrichment/litellm_enrichment_provider.py → enricher/litellm_enricher.py} +36 -56
  73. kodit/infrastructure/{enrichment/local_enrichment_provider.py → enricher/local_enricher.py} +19 -24
  74. kodit/infrastructure/enricher/null_enricher.py +36 -0
  75. kodit/infrastructure/indexing/fusion_service.py +1 -1
  76. kodit/infrastructure/mappers/enrichment_mapper.py +83 -0
  77. kodit/infrastructure/mappers/git_mapper.py +193 -0
  78. kodit/infrastructure/mappers/snippet_mapper.py +104 -0
  79. kodit/infrastructure/mappers/task_mapper.py +5 -44
  80. kodit/infrastructure/physical_architecture/__init__.py +1 -0
  81. kodit/infrastructure/physical_architecture/detectors/__init__.py +1 -0
  82. kodit/infrastructure/physical_architecture/detectors/docker_compose_detector.py +336 -0
  83. kodit/infrastructure/physical_architecture/formatters/__init__.py +1 -0
  84. kodit/infrastructure/physical_architecture/formatters/narrative_formatter.py +149 -0
  85. kodit/infrastructure/reporting/log_progress.py +8 -5
  86. kodit/infrastructure/reporting/telemetry_progress.py +21 -0
  87. kodit/infrastructure/slicing/api_doc_extractor.py +836 -0
  88. kodit/infrastructure/slicing/ast_analyzer.py +1128 -0
  89. kodit/infrastructure/slicing/slicer.py +87 -421
  90. kodit/infrastructure/sqlalchemy/embedding_repository.py +43 -23
  91. kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +118 -0
  92. kodit/infrastructure/sqlalchemy/entities.py +402 -158
  93. kodit/infrastructure/sqlalchemy/git_branch_repository.py +274 -0
  94. kodit/infrastructure/sqlalchemy/git_commit_repository.py +346 -0
  95. kodit/infrastructure/sqlalchemy/git_repository.py +262 -0
  96. kodit/infrastructure/sqlalchemy/git_tag_repository.py +268 -0
  97. kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +479 -0
  98. kodit/infrastructure/sqlalchemy/task_repository.py +29 -23
  99. kodit/infrastructure/sqlalchemy/task_status_repository.py +24 -12
  100. kodit/infrastructure/sqlalchemy/unit_of_work.py +10 -14
  101. kodit/mcp.py +12 -30
  102. kodit/migrations/env.py +1 -0
  103. kodit/migrations/versions/04b80f802e0c_foreign_key_review.py +100 -0
  104. kodit/migrations/versions/19f8c7faf8b9_add_generic_enrichment_type.py +260 -0
  105. kodit/migrations/versions/7f15f878c3a1_add_new_git_entities.py +690 -0
  106. kodit/migrations/versions/f9e5ef5e688f_add_git_commits_number.py +43 -0
  107. kodit/py.typed +0 -0
  108. kodit/utils/dump_config.py +361 -0
  109. kodit/utils/dump_openapi.py +6 -4
  110. kodit/utils/path_utils.py +29 -0
  111. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/METADATA +3 -3
  112. kodit-0.5.1.dist-info/RECORD +168 -0
  113. kodit/application/factories/code_indexing_factory.py +0 -195
  114. kodit/application/services/auto_indexing_service.py +0 -99
  115. kodit/application/services/code_indexing_application_service.py +0 -410
  116. kodit/domain/services/index_query_service.py +0 -70
  117. kodit/domain/services/index_service.py +0 -269
  118. kodit/infrastructure/api/client/index_client.py +0 -57
  119. kodit/infrastructure/api/v1/routers/indexes.py +0 -164
  120. kodit/infrastructure/api/v1/schemas/index.py +0 -101
  121. kodit/infrastructure/bm25/bm25_factory.py +0 -28
  122. kodit/infrastructure/cloning/__init__.py +0 -1
  123. kodit/infrastructure/cloning/metadata.py +0 -98
  124. kodit/infrastructure/enrichment/__init__.py +0 -1
  125. kodit/infrastructure/enrichment/enrichment_factory.py +0 -52
  126. kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -19
  127. kodit/infrastructure/mappers/index_mapper.py +0 -345
  128. kodit/infrastructure/reporting/tdqm_progress.py +0 -38
  129. kodit/infrastructure/slicing/language_detection_service.py +0 -18
  130. kodit/infrastructure/sqlalchemy/index_repository.py +0 -646
  131. kodit-0.4.3.dist-info/RECORD +0 -125
  132. /kodit/infrastructure/{enrichment → enricher}/utils.py +0 -0
  133. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/WHEEL +0 -0
  134. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/entry_points.txt +0 -0
  135. {kodit-0.4.3.dist-info → kodit-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,336 @@
1
+ """Docker Compose detector for physical architecture discovery."""
2
+
3
+ import contextlib
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ class DockerComposeDetector:
11
+ """Detects physical components from Docker Compose files and generates narrative observations.""" # noqa: E501
12
+
13
+ # Regex pattern to detect communication addresses in environment variables
14
+ # Matches complete URLs with hostnames:
15
+ # - Simple URLs: http://api:8080, redis://cache:6379
16
+ # - Connection strings with auth: postgresql://user:pass@db:5432/dbname
17
+ # - Connection strings with asyncpg: postgresql+asyncpg://user:pass@db:5432
18
+ # Note: This captures the hostname portion, avoiding false matches in
19
+ # passwords or other parts of the URL
20
+ COMMUNICATION_PATTERN = re.compile(
21
+ r"(?:"
22
+ # Protocol-based URLs with optional auth (user:pass@)
23
+ r"(?:https?|tcp|grpc|ws|wss|amqp|kafka|redis|memcached|"
24
+ r"postgres(?:ql)?(?:\+\w+)?|mysql|mongodb)://"
25
+ r"(?:[^@/]+@)?" # Optional user:pass@ (non-capturing, skip it)
26
+ r"([\w\-\.]+(?::\d+)?)" # Capture hostname:port after @ or ://
27
+ r")",
28
+ re.IGNORECASE,
29
+ )
30
+
31
+ async def analyze(self, repo_path: Path) -> tuple[list[str], list[str], list[str]]:
32
+ """Generate narrative observations from Docker Compose analysis."""
33
+ component_notes: list[str] = []
34
+ connection_notes: list[str] = []
35
+ infrastructure_notes: list[str] = []
36
+
37
+ # Find all docker-compose files
38
+ yml_files = list(repo_path.glob("docker-compose*.yml"))
39
+ yaml_files = list(repo_path.glob("docker-compose*.yaml"))
40
+ compose_files = yml_files + yaml_files
41
+
42
+ if not compose_files:
43
+ return ([], [], [])
44
+
45
+ # Analyze each compose file
46
+ for compose_file in compose_files:
47
+ try:
48
+ with compose_file.open(encoding="utf-8") as f:
49
+ compose_data = yaml.safe_load(f)
50
+
51
+ if not compose_data or "services" not in compose_data:
52
+ continue
53
+
54
+ self._analyze_compose_file(
55
+ compose_file,
56
+ compose_data,
57
+ component_notes,
58
+ connection_notes,
59
+ infrastructure_notes,
60
+ )
61
+
62
+ except (yaml.YAMLError, OSError, KeyError):
63
+ infrastructure_notes.append(
64
+ f"Unable to parse Docker Compose file at {compose_file}. "
65
+ "File may be malformed or inaccessible."
66
+ )
67
+
68
+ return (component_notes, connection_notes, infrastructure_notes)
69
+
70
+ def _analyze_compose_file(
71
+ self,
72
+ compose_file: Path,
73
+ compose_data: dict,
74
+ component_notes: list[str],
75
+ connection_notes: list[str],
76
+ infrastructure_notes: list[str],
77
+ ) -> None:
78
+ """Analyze a single Docker Compose file and generate observations."""
79
+ services = compose_data.get("services", {})
80
+
81
+ # High-level infrastructure observation
82
+ infrastructure_notes.append(
83
+ f"Found Docker Compose configuration at {compose_file.name} defining "
84
+ f"{len(services)} services. This suggests a containerized application "
85
+ f"architecture with orchestrated service dependencies."
86
+ )
87
+
88
+ # Analyze each service
89
+ for service_name, service_config in services.items():
90
+ self._analyze_service(
91
+ service_name,
92
+ service_config,
93
+ component_notes,
94
+ connection_notes,
95
+ )
96
+
97
+ # Analyze service dependencies
98
+ self._analyze_service_dependencies(services, connection_notes)
99
+
100
+ # Check for additional Docker Compose features
101
+ self._analyze_compose_features(compose_data, infrastructure_notes)
102
+
103
+ def _analyze_service(
104
+ self,
105
+ service_name: str,
106
+ service_config: dict,
107
+ component_notes: list[str],
108
+ _connection_notes: list[str],
109
+ ) -> None:
110
+ """Generate narrative observations for a single service."""
111
+ # Extract key configuration details
112
+ image = service_config.get("image", "")
113
+ build = service_config.get("build", "")
114
+ ports = self._extract_ports(service_config)
115
+
116
+ component_observation = (
117
+ f"Found '{service_name}' service in Docker Compose configuration."
118
+ )
119
+
120
+ # Add deployment details
121
+ if image:
122
+ component_observation += f" Service uses '{image}' Docker image"
123
+ if ":" in image:
124
+ tag = image.split(":")[-1]
125
+ component_observation += f" with tag '{tag}'"
126
+ component_observation += "."
127
+ elif build:
128
+ component_observation += f" Service builds from local source at '{build}'."
129
+
130
+ # Add port information
131
+ if ports:
132
+ port_list = ", ".join(str(p) for p in ports)
133
+ component_observation += f" Exposes ports {port_list}"
134
+ protocol_info = self._infer_protocol_description(ports)
135
+ if protocol_info:
136
+ component_observation += f" suggesting {protocol_info}"
137
+ component_observation += "."
138
+
139
+ component_notes.append(component_observation)
140
+
141
+ def _analyze_service_dependencies( # noqa: PLR0912, C901
142
+ self, services: dict, connection_notes: list[str]
143
+ ) -> None:
144
+ """Analyze dependencies between services."""
145
+ for service_name, service_config in services.items():
146
+ depends_on = service_config.get("depends_on", [])
147
+
148
+ if isinstance(depends_on, dict):
149
+ dependencies = list(depends_on.keys())
150
+ condition_info = []
151
+ for dep, condition in depends_on.items():
152
+ if isinstance(condition, dict) and "condition" in condition:
153
+ condition_info.append(f"{dep} ({condition['condition']})")
154
+
155
+ if condition_info:
156
+ connection_notes.append(
157
+ f"Service '{service_name}' has conditional dependencies on "
158
+ f"{', '.join(condition_info)}, indicating sophisticated "
159
+ "startup orchestration with health checks."
160
+ )
161
+ else:
162
+ dependencies = list(depends_on.keys())
163
+ elif isinstance(depends_on, list):
164
+ dependencies = depends_on
165
+ else:
166
+ continue
167
+
168
+ if dependencies:
169
+ dep_list = "', '".join(dependencies)
170
+ connection_notes.append(
171
+ f"Docker Compose 'depends_on' configuration shows '{service_name}' "
172
+ f"requires '{dep_list}' to start first, indicating service startup "
173
+ "dependency and likely runtime communication pattern."
174
+ )
175
+
176
+ # Check for communication patterns in environment variables
177
+ # and command arguments
178
+ service_names = {name for name, _ in services.items()}
179
+ # Track which connections we've already recorded to avoid duplicates
180
+ recorded_connections: set[tuple[str, str]] = set()
181
+
182
+ for service_name, service_config in services.items():
183
+ # Check environment variables
184
+ env = service_config.get("environment", [])
185
+ if isinstance(env, list):
186
+ for var in env:
187
+ self._check_communication_pattern(
188
+ var,
189
+ service_name,
190
+ service_names,
191
+ "environment variable",
192
+ connection_notes,
193
+ recorded_connections,
194
+ )
195
+ elif isinstance(env, dict):
196
+ for value in env.values():
197
+ if isinstance(value, str):
198
+ self._check_communication_pattern(
199
+ value,
200
+ service_name,
201
+ service_names,
202
+ "environment variable",
203
+ connection_notes,
204
+ recorded_connections,
205
+ )
206
+
207
+ # Check command arguments
208
+ args = service_config.get("command", [])
209
+ if isinstance(args, list):
210
+ for arg in args:
211
+ if isinstance(arg, str):
212
+ self._check_communication_pattern(
213
+ arg,
214
+ service_name,
215
+ service_names,
216
+ "command argument",
217
+ connection_notes,
218
+ recorded_connections,
219
+ )
220
+ elif isinstance(args, str):
221
+ self._check_communication_pattern(
222
+ args,
223
+ service_name,
224
+ service_names,
225
+ "command argument",
226
+ connection_notes,
227
+ recorded_connections,
228
+ )
229
+
230
+ def _check_communication_pattern( # noqa: PLR0913
231
+ self,
232
+ text: str,
233
+ service_name: str,
234
+ service_names: set[str],
235
+ source_type: str,
236
+ connection_notes: list[str],
237
+ recorded_connections: set[tuple[str, str]],
238
+ ) -> None:
239
+ """Check if text contains communication patterns referencing other services."""
240
+ # Find all matches and extract hostnames from captured groups
241
+ matches = self.COMMUNICATION_PATTERN.finditer(text)
242
+ hostnames = set()
243
+
244
+ for match in matches:
245
+ # Group 1 contains the hostname
246
+ if match.group(1):
247
+ # Extract just the hostname (without port)
248
+ hostname = match.group(1).split(":")[0]
249
+ hostnames.add(hostname)
250
+
251
+ if not hostnames:
252
+ return
253
+
254
+ # Check if any extracted hostname matches a service name
255
+ for target_service in service_names:
256
+ if target_service == service_name:
257
+ continue
258
+
259
+ # Check if the target service is in the extracted hostnames
260
+ if target_service in hostnames:
261
+ connection_key = (service_name, target_service)
262
+ if connection_key not in recorded_connections:
263
+ connection_notes.append(
264
+ f"'{service_name}' has a communication address referencing "
265
+ f"'{target_service}' in its {source_type}, indicating a "
266
+ "direct runtime dependency."
267
+ )
268
+ recorded_connections.add(connection_key)
269
+ break
270
+
271
+ def _analyze_compose_features(
272
+ self, compose_data: dict, infrastructure_notes: list[str]
273
+ ) -> None:
274
+ """Analyze additional Docker Compose features."""
275
+ # Check for networks
276
+ networks = compose_data.get("networks", {})
277
+ if networks:
278
+ infrastructure_notes.append(
279
+ f"Docker Compose defines {len(networks)} custom networks, "
280
+ "indicating network segmentation and controlled service communication."
281
+ )
282
+
283
+ def _extract_ports(self, service_config: dict) -> list[int]:
284
+ """Extract port numbers from service configuration."""
285
+ ports = []
286
+
287
+ # Extract from 'ports' section
288
+ port_specs = service_config.get("ports", [])
289
+ for port_spec in port_specs:
290
+ if isinstance(port_spec, str):
291
+ if ":" in port_spec:
292
+ external_port = port_spec.split(":")[0]
293
+ with contextlib.suppress(ValueError):
294
+ ports.append(int(external_port))
295
+ else:
296
+ with contextlib.suppress(ValueError):
297
+ ports.append(int(port_spec))
298
+ elif isinstance(port_spec, int):
299
+ ports.append(port_spec)
300
+
301
+ # Extract from 'expose' section
302
+ expose_specs = service_config.get("expose", [])
303
+ for expose_spec in expose_specs:
304
+ with contextlib.suppress(ValueError, TypeError):
305
+ ports.append(int(expose_spec))
306
+
307
+ return sorted(set(ports))
308
+
309
+ def _infer_protocol_description(self, ports: list[int]) -> str:
310
+ """Infer protocol information from ports and return descriptive text."""
311
+ protocols = []
312
+
313
+ # HTTP ports
314
+ http_ports = {80, 8080, 3000, 4200, 5000, 8000, 8443, 443}
315
+ if any(port in http_ports for port in ports):
316
+ protocols.append("HTTP/HTTPS web traffic")
317
+
318
+ # gRPC ports
319
+ grpc_ports = {9090, 50051}
320
+ if any(port in grpc_ports for port in ports):
321
+ protocols.append("gRPC API communication")
322
+
323
+ # Cache/Redis ports
324
+ if 6379 in ports:
325
+ protocols.append("cache service")
326
+
327
+ # Database ports (excluding Redis which is handled above)
328
+ db_ports = {5432, 3306, 27017}
329
+ if any(port in db_ports for port in ports):
330
+ protocols.append("database service")
331
+
332
+ if protocols:
333
+ return " and ".join(protocols)
334
+ if ports:
335
+ return "TCP-based service communication"
336
+ return ""
@@ -0,0 +1 @@
1
+ """Formatters for converting architecture observations to LLM-optimized text."""
@@ -0,0 +1,149 @@
1
+ """Narrative formatter for converting observations to LLM-optimized text."""
2
+
3
+ import re
4
+
5
+ from kodit.domain.enrichments.architecture.physical.discovery_notes import (
6
+ ArchitectureDiscoveryNotes,
7
+ )
8
+
9
+
10
+ class NarrativeFormatter:
11
+ """Formats architecture observations into narrative text optimized for LLM consumption.""" # noqa: E501
12
+
13
+ def format_for_llm(self, notes: ArchitectureDiscoveryNotes) -> str:
14
+ """Convert discovery notes into a comprehensive narrative format."""
15
+ sections = []
16
+
17
+ # Title and overview
18
+ sections.append("# Physical Architecture Discovery Report")
19
+ sections.append("")
20
+ sections.append(notes.repository_context)
21
+ sections.append("")
22
+
23
+ # Component Analysis
24
+ self._add_component_section(sections, notes.component_observations)
25
+
26
+ # Connection Analysis
27
+ self._add_connection_section(sections, notes.connection_observations)
28
+
29
+ # Infrastructure Analysis
30
+ self._add_infrastructure_section(sections, notes.infrastructure_observations)
31
+
32
+ # Methodology
33
+ sections.append("## Discovery Methodology")
34
+ sections.append(notes.discovery_metadata)
35
+ sections.append("")
36
+
37
+ # Conclusion
38
+ self._add_conclusion_section(sections, notes)
39
+
40
+ return "\n".join(sections)
41
+
42
+ def _add_component_section(
43
+ self, sections: list[str], component_observations: list[str]
44
+ ) -> None:
45
+ """Add component observations section with proper formatting."""
46
+ sections.append("## Components")
47
+ sections.append("")
48
+
49
+ if component_observations:
50
+ for i, observation in enumerate(component_observations, 1):
51
+ sections.append(f"**{i}.** {observation}")
52
+ sections.append("")
53
+
54
+ # Extract and highlight port information
55
+ port_info = self._extract_port_information(component_observations)
56
+ if port_info:
57
+ sections.append("### Port Mappings")
58
+ sections.append("")
59
+ for component, ports_desc in port_info.items():
60
+ sections.append(f"- **{component}**: {ports_desc}")
61
+ sections.append("")
62
+ else:
63
+ sections.append("None. Likely monolithic or library architecture.")
64
+ sections.append("")
65
+
66
+ def _add_connection_section(
67
+ self, sections: list[str], connection_observations: list[str]
68
+ ) -> None:
69
+ """Add connection observations section with proper formatting."""
70
+ sections.append("## Connections")
71
+ sections.append("")
72
+
73
+ if connection_observations:
74
+ for i, observation in enumerate(connection_observations, 1):
75
+ sections.append(f"**{i}.** {observation}")
76
+ sections.append("")
77
+ else:
78
+ sections.append("None. Possible monolithic or independent services.")
79
+ sections.append("")
80
+
81
+ def _add_infrastructure_section(
82
+ self, sections: list[str], infrastructure_observations: list[str]
83
+ ) -> None:
84
+ """Add infrastructure observations section with proper formatting."""
85
+ sections.append("## Infrastructure")
86
+ sections.append("")
87
+
88
+ if infrastructure_observations:
89
+ for i, observation in enumerate(infrastructure_observations, 1):
90
+ sections.append(f"**{i}.** {observation}")
91
+ sections.append("")
92
+ else:
93
+ sections.append("None. May use external or cloud-native deployment.")
94
+ sections.append("")
95
+
96
+ def _add_conclusion_section(
97
+ self, sections: list[str], notes: ArchitectureDiscoveryNotes
98
+ ) -> None:
99
+ """Add a conclusion section summarizing the findings."""
100
+ sections.append("## Summary")
101
+ sections.append("")
102
+
103
+ # Determine architecture characteristics
104
+ has_components = bool(notes.component_observations)
105
+ has_connections = bool(notes.connection_observations)
106
+ has_infrastructure = bool(notes.infrastructure_observations)
107
+
108
+ if has_components and has_connections and has_infrastructure:
109
+ arch_type = "distributed microservices"
110
+ complexity = "high"
111
+ elif has_components and (has_connections or has_infrastructure):
112
+ arch_type = "multi-component"
113
+ complexity = "medium"
114
+ elif has_components or has_infrastructure:
115
+ arch_type = "structured application"
116
+ complexity = "medium"
117
+ else:
118
+ arch_type = "monolithic"
119
+ complexity = "low"
120
+
121
+ sections.append(f"**Architecture:** {arch_type} | **Complexity:** {complexity}")
122
+ sections.append("")
123
+ sections.append("**Note:** Static analysis only. Runtime behavior may differ.")
124
+
125
+ def _extract_port_information(
126
+ self, component_observations: list[str]
127
+ ) -> dict[str, str]:
128
+ """Extract port information from component observations."""
129
+ port_info = {}
130
+
131
+ # Pattern to extract service name and port information
132
+ service_pattern = r"Found '([^']+)' service"
133
+ port_pattern = r"Exposes ports ([\d, ]+)(?: suggesting ([^.]+))?"
134
+
135
+ for observation in component_observations:
136
+ service_match = re.search(service_pattern, observation)
137
+ port_match = re.search(port_pattern, observation)
138
+
139
+ if service_match and port_match:
140
+ service_name = service_match.group(1)
141
+ ports = port_match.group(1).strip()
142
+ protocol = port_match.group(2)
143
+
144
+ if protocol:
145
+ port_info[service_name] = f"{ports} ({protocol})"
146
+ else:
147
+ port_info[service_name] = ports
148
+
149
+ return port_info
@@ -22,13 +22,16 @@ class LoggingReportingModule(ReportingModule):
22
22
  async def on_change(self, progress: TaskStatus) -> None:
23
23
  """On step changed."""
24
24
  current_time = datetime.now(UTC)
25
- time_since_last_log = current_time - self._last_log_time
26
25
  step = progress
27
26
 
28
- if (
29
- step.state != ReportingState.IN_PROGRESS
30
- or time_since_last_log >= self.config.log_time_interval
31
- ):
27
+ if step.state == ReportingState.FAILED:
28
+ self._log.exception(
29
+ step.operation,
30
+ state=step.state,
31
+ completion_percent=step.completion_percent,
32
+ error=step.error,
33
+ )
34
+ else:
32
35
  self._log.info(
33
36
  step.operation,
34
37
  state=step.state,
@@ -0,0 +1,21 @@
1
+ """Log progress using telemetry."""
2
+
3
+ import structlog
4
+
5
+ from kodit.domain.entities import TaskStatus
6
+ from kodit.domain.protocols import ReportingModule
7
+ from kodit.log import log_event
8
+
9
+
10
+ class TelemetryProgressReportingModule(ReportingModule):
11
+ """Database progress reporting module."""
12
+
13
+ def __init__(self) -> None:
14
+ """Initialize the logging reporting module."""
15
+ self._log = structlog.get_logger(__name__)
16
+
17
+ async def on_change(self, progress: TaskStatus) -> None:
18
+ """On step changed."""
19
+ log_event(
20
+ progress.operation,
21
+ )