kodit 0.5.0__py3-none-any.whl → 0.5.2__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 (70) hide show
  1. kodit/_version.py +2 -2
  2. kodit/app.py +10 -12
  3. kodit/application/factories/server_factory.py +78 -11
  4. kodit/application/services/commit_indexing_application_service.py +188 -31
  5. kodit/application/services/enrichment_query_service.py +95 -0
  6. kodit/config.py +3 -3
  7. kodit/domain/enrichments/__init__.py +1 -0
  8. kodit/domain/enrichments/architecture/__init__.py +1 -0
  9. kodit/domain/enrichments/architecture/architecture.py +20 -0
  10. kodit/domain/enrichments/architecture/physical/__init__.py +1 -0
  11. kodit/domain/enrichments/architecture/physical/discovery_notes.py +14 -0
  12. kodit/domain/enrichments/architecture/physical/formatter.py +11 -0
  13. kodit/domain/enrichments/architecture/physical/physical.py +17 -0
  14. kodit/domain/enrichments/development/__init__.py +1 -0
  15. kodit/domain/enrichments/development/development.py +18 -0
  16. kodit/domain/enrichments/development/snippet/__init__.py +1 -0
  17. kodit/domain/enrichments/development/snippet/snippet.py +21 -0
  18. kodit/domain/enrichments/enricher.py +17 -0
  19. kodit/domain/enrichments/enrichment.py +39 -0
  20. kodit/domain/enrichments/request.py +12 -0
  21. kodit/domain/enrichments/response.py +11 -0
  22. kodit/domain/enrichments/usage/__init__.py +1 -0
  23. kodit/domain/enrichments/usage/api_docs.py +19 -0
  24. kodit/domain/enrichments/usage/usage.py +18 -0
  25. kodit/domain/protocols.py +7 -6
  26. kodit/domain/services/enrichment_service.py +9 -30
  27. kodit/domain/services/physical_architecture_service.py +182 -0
  28. kodit/domain/tracking/__init__.py +1 -0
  29. kodit/domain/tracking/resolution_service.py +81 -0
  30. kodit/domain/tracking/trackable.py +21 -0
  31. kodit/domain/value_objects.py +6 -23
  32. kodit/infrastructure/api/v1/dependencies.py +15 -0
  33. kodit/infrastructure/api/v1/routers/commits.py +81 -0
  34. kodit/infrastructure/api/v1/routers/repositories.py +99 -0
  35. kodit/infrastructure/api/v1/schemas/enrichment.py +29 -0
  36. kodit/infrastructure/cloning/git/git_python_adaptor.py +71 -4
  37. kodit/infrastructure/enricher/__init__.py +1 -0
  38. kodit/infrastructure/enricher/enricher_factory.py +53 -0
  39. kodit/infrastructure/{enrichment/litellm_enrichment_provider.py → enricher/litellm_enricher.py} +20 -33
  40. kodit/infrastructure/{enrichment/local_enrichment_provider.py → enricher/local_enricher.py} +19 -24
  41. kodit/infrastructure/enricher/null_enricher.py +36 -0
  42. kodit/infrastructure/mappers/enrichment_mapper.py +83 -0
  43. kodit/infrastructure/mappers/snippet_mapper.py +20 -22
  44. kodit/infrastructure/physical_architecture/__init__.py +1 -0
  45. kodit/infrastructure/physical_architecture/detectors/__init__.py +1 -0
  46. kodit/infrastructure/physical_architecture/detectors/docker_compose_detector.py +336 -0
  47. kodit/infrastructure/physical_architecture/formatters/__init__.py +1 -0
  48. kodit/infrastructure/physical_architecture/formatters/narrative_formatter.py +149 -0
  49. kodit/infrastructure/slicing/api_doc_extractor.py +836 -0
  50. kodit/infrastructure/slicing/ast_analyzer.py +1128 -0
  51. kodit/infrastructure/slicing/slicer.py +56 -391
  52. kodit/infrastructure/sqlalchemy/enrichment_v2_repository.py +118 -0
  53. kodit/infrastructure/sqlalchemy/entities.py +46 -38
  54. kodit/infrastructure/sqlalchemy/git_branch_repository.py +22 -11
  55. kodit/infrastructure/sqlalchemy/git_commit_repository.py +23 -14
  56. kodit/infrastructure/sqlalchemy/git_repository.py +27 -17
  57. kodit/infrastructure/sqlalchemy/git_tag_repository.py +22 -11
  58. kodit/infrastructure/sqlalchemy/snippet_v2_repository.py +101 -106
  59. kodit/migrations/versions/19f8c7faf8b9_add_generic_enrichment_type.py +260 -0
  60. kodit/utils/dump_config.py +361 -0
  61. kodit/utils/dump_openapi.py +5 -6
  62. {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/METADATA +1 -1
  63. {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/RECORD +67 -32
  64. kodit/infrastructure/enrichment/__init__.py +0 -1
  65. kodit/infrastructure/enrichment/enrichment_factory.py +0 -52
  66. kodit/infrastructure/enrichment/null_enrichment_provider.py +0 -19
  67. /kodit/infrastructure/{enrichment → enricher}/utils.py +0 -0
  68. {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/WHEEL +0 -0
  69. {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/entry_points.txt +0 -0
  70. {kodit-0.5.0.dist-info → kodit-0.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,14 @@
1
+ """Physical architecture domain value objects."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class ArchitectureDiscoveryNotes:
8
+ """Rich, narrative observations about repository architecture for LLM consumption.""" # noqa: E501
9
+
10
+ repository_context: str # High-level overview and discovery scope
11
+ component_observations: list[str] # Detailed findings about each component
12
+ connection_observations: list[str] # How components interact and communicate
13
+ infrastructure_observations: list[str] # Deployment, config, operational patterns
14
+ discovery_metadata: str # Methodology, confidence, limitations, timestamp
@@ -0,0 +1,11 @@
1
+ """Physical architecture formatter protocol."""
2
+
3
+ from typing import Any, Protocol
4
+
5
+
6
+ class PhysicalArchitectureFormatter(Protocol):
7
+ """Formatter for converting architecture discovery notes to LLM-optimized text."""
8
+
9
+ def format_for_llm(self, notes: Any) -> str:
10
+ """Format architecture discovery notes for LLM consumption."""
11
+ ...
@@ -0,0 +1,17 @@
1
+ """Physical architecture enrichment domain entity."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from kodit.domain.enrichments.architecture.architecture import ArchitectureEnrichment
6
+
7
+ ENRICHMENT_SUBTYPE_PHYSICAL = "physical"
8
+
9
+
10
+ @dataclass
11
+ class PhysicalArchitectureEnrichment(ArchitectureEnrichment):
12
+ """Enrichment containing physical architecture discovery for a commit."""
13
+
14
+ @property
15
+ def subtype(self) -> str | None:
16
+ """Return the enrichment subtype."""
17
+ return ENRICHMENT_SUBTYPE_PHYSICAL
@@ -0,0 +1 @@
1
+ """Development enrichment package."""
@@ -0,0 +1,18 @@
1
+ """Development enrichment domain entity."""
2
+
3
+ from abc import ABC
4
+ from dataclasses import dataclass
5
+
6
+ from kodit.domain.enrichments.enrichment import CommitEnrichment
7
+
8
+ ENRICHMENT_TYPE_DEVELOPMENT = "development"
9
+
10
+
11
+ @dataclass
12
+ class DevelopmentEnrichment(CommitEnrichment, ABC):
13
+ """Enrichment containing development discovery for a commit."""
14
+
15
+ @property
16
+ def type(self) -> str:
17
+ """Return the enrichment type."""
18
+ return ENRICHMENT_TYPE_DEVELOPMENT
@@ -0,0 +1 @@
1
+ """Snippet enrichment package."""
@@ -0,0 +1,21 @@
1
+ """Snippet enrichment domain entity."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from kodit.domain.enrichments.development.development import DevelopmentEnrichment
6
+
7
+ ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY = "snippet_summary"
8
+
9
+
10
+ @dataclass
11
+ class SnippetEnrichment(DevelopmentEnrichment):
12
+ """Enrichment specific to code snippets."""
13
+
14
+ @property
15
+ def subtype(self) -> str | None:
16
+ """Return the enrichment subtype."""
17
+ return ENRICHMENT_SUBTYPE_SNIPPET_SUMMARY
18
+
19
+ def entity_type_key(self) -> str:
20
+ """Return the entity type key this enrichment is for."""
21
+ return "snippet_v2"
@@ -0,0 +1,17 @@
1
+ """Enricher interface."""
2
+
3
+ from collections.abc import AsyncGenerator
4
+ from typing import Protocol
5
+
6
+ from kodit.domain.enrichments.request import EnrichmentRequest
7
+ from kodit.domain.enrichments.response import EnrichmentResponse
8
+
9
+
10
+ class Enricher(Protocol):
11
+ """Interface for text enrichment with custom prompts."""
12
+
13
+ def enrich(
14
+ self, requests: list[EnrichmentRequest]
15
+ ) -> AsyncGenerator[EnrichmentResponse, None]:
16
+ """Enrich a list of requests with custom system prompts."""
17
+ ...
@@ -0,0 +1,39 @@
1
+ """Enrichment domain entities."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+
7
+
8
+ @dataclass
9
+ class EnrichmentV2(ABC):
10
+ """Generic enrichment that can be attached to any entity."""
11
+
12
+ entity_id: str
13
+ content: str = ""
14
+ id: int | None = None
15
+ created_at: datetime | None = None
16
+ updated_at: datetime | None = None
17
+
18
+ @property
19
+ @abstractmethod
20
+ def type(self) -> str:
21
+ """Return the enrichment type."""
22
+
23
+ @property
24
+ @abstractmethod
25
+ def subtype(self) -> str | None:
26
+ """Return the enrichment subtype (optional for hierarchical types)."""
27
+
28
+ @abstractmethod
29
+ def entity_type_key(self) -> str:
30
+ """Return the entity type key this enrichment is for."""
31
+
32
+
33
+ @dataclass
34
+ class CommitEnrichment(EnrichmentV2, ABC):
35
+ """Enrichment specific to commits."""
36
+
37
+ def entity_type_key(self) -> str:
38
+ """Return the entity type key this enrichment is for."""
39
+ return "git_commit"
@@ -0,0 +1,12 @@
1
+ """Generic enrichment request value object."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class EnrichmentRequest:
8
+ """Domain model for generic enrichment request with custom prompt."""
9
+
10
+ id: str
11
+ text: str
12
+ system_prompt: str
@@ -0,0 +1,11 @@
1
+ """Generic enrichment response value object."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class EnrichmentResponse:
8
+ """Domain model for generic enrichment response."""
9
+
10
+ id: str
11
+ text: str
@@ -0,0 +1 @@
1
+ """Usage enrichment domain entities."""
@@ -0,0 +1,19 @@
1
+ """API documentation enrichment entity."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from kodit.domain.enrichments.usage.usage import UsageEnrichment
6
+
7
+ ENRICHMENT_SUBTYPE_API_DOCS = "api_docs"
8
+
9
+
10
+ @dataclass
11
+ class APIDocEnrichment(UsageEnrichment):
12
+ """API documentation enrichment for a module."""
13
+
14
+ language: str = ""
15
+
16
+ @property
17
+ def subtype(self) -> str | None:
18
+ """Return the enrichment subtype."""
19
+ return ENRICHMENT_SUBTYPE_API_DOCS
@@ -0,0 +1,18 @@
1
+ """Usage enrichment domain entity."""
2
+
3
+ from abc import ABC
4
+ from dataclasses import dataclass
5
+
6
+ from kodit.domain.enrichments.enrichment import CommitEnrichment
7
+
8
+ ENRICHMENT_TYPE_USAGE = "usage"
9
+
10
+
11
+ @dataclass
12
+ class UsageEnrichment(CommitEnrichment, ABC):
13
+ """Enrichment containing development discovery for a commit."""
14
+
15
+ @property
16
+ def type(self) -> str:
17
+ """Return the enrichment type."""
18
+ return ENRICHMENT_TYPE_USAGE
kodit/domain/protocols.py CHANGED
@@ -217,7 +217,6 @@ class GitRepoRepository(ABC):
217
217
  """Delete a repository."""
218
218
 
219
219
 
220
-
221
220
  class GitAdapter(ABC):
222
221
  """Abstract interface for Git operations."""
223
222
 
@@ -225,10 +224,6 @@ class GitAdapter(ABC):
225
224
  async def clone_repository(self, remote_uri: str, local_path: Path) -> None:
226
225
  """Clone a repository to local path."""
227
226
 
228
- @abstractmethod
229
- async def checkout_commit(self, local_path: Path, commit_sha: str) -> None:
230
- """Checkout a specific commit in the repository."""
231
-
232
227
  @abstractmethod
233
228
  async def pull_repository(self, local_path: Path) -> None:
234
229
  """Pull latest changes for existing repository."""
@@ -247,7 +242,13 @@ class GitAdapter(ABC):
247
242
  async def get_commit_files(
248
243
  self, local_path: Path, commit_sha: str
249
244
  ) -> list[dict[str, Any]]:
250
- """Get all files in a specific commit."""
245
+ """Get all files in a specific commit from the git tree."""
246
+
247
+ @abstractmethod
248
+ async def get_commit_file_data(
249
+ self, local_path: Path, commit_sha: str
250
+ ) -> list[dict[str, Any]]:
251
+ """Get file metadata for a commit, with files checked out to disk."""
251
252
 
252
253
  @abstractmethod
253
254
  async def repository_exists(self, local_path: Path) -> bool:
@@ -1,48 +1,27 @@
1
1
  """Domain service for enrichment operations."""
2
2
 
3
- from abc import ABC, abstractmethod
4
3
  from collections.abc import AsyncGenerator
5
4
 
6
- from kodit.domain.value_objects import (
7
- EnrichmentIndexRequest,
8
- EnrichmentRequest,
9
- EnrichmentResponse,
10
- )
11
-
12
-
13
- class EnrichmentProvider(ABC):
14
- """Abstract enrichment provider interface."""
15
-
16
- @abstractmethod
17
- def enrich(
18
- self, requests: list[EnrichmentRequest]
19
- ) -> AsyncGenerator[EnrichmentResponse, None]:
20
- """Enrich a list of requests."""
5
+ from kodit.domain.enrichments.enricher import Enricher
6
+ from kodit.domain.enrichments.request import EnrichmentRequest
7
+ from kodit.domain.enrichments.response import EnrichmentResponse
21
8
 
22
9
 
23
10
  class EnrichmentDomainService:
24
11
  """Domain service for enrichment operations."""
25
12
 
26
- def __init__(self, enrichment_provider: EnrichmentProvider) -> None:
27
- """Initialize the enrichment domain service.
28
-
29
- Args:
30
- enrichment_provider: The enrichment provider to use.
31
-
32
- """
33
- self.enrichment_provider = enrichment_provider
13
+ def __init__(self, enricher: Enricher) -> None:
14
+ """Initialize the enrichment domain service."""
15
+ self.enricher = enricher
34
16
 
35
17
  async def enrich_documents(
36
- self, request: EnrichmentIndexRequest
18
+ self, requests: list[EnrichmentRequest]
37
19
  ) -> AsyncGenerator[EnrichmentResponse, None]:
38
- """Enrich documents using the enrichment provider.
39
-
40
- Args:
41
- request: The enrichment index request.
20
+ """Enrich documents using the enricher.
42
21
 
43
22
  Yields:
44
23
  Enrichment responses as they are processed.
45
24
 
46
25
  """
47
- async for response in self.enrichment_provider.enrich(request.requests):
26
+ async for response in self.enricher.enrich(requests):
48
27
  yield response
@@ -0,0 +1,182 @@
1
+ """Core service for discovering physical architecture and generating narrative observations.""" # noqa: E501
2
+
3
+ from datetime import UTC, datetime
4
+ from pathlib import Path
5
+
6
+ from kodit.domain.enrichments.architecture.physical.discovery_notes import (
7
+ ArchitectureDiscoveryNotes,
8
+ )
9
+ from kodit.domain.enrichments.architecture.physical.formatter import (
10
+ PhysicalArchitectureFormatter,
11
+ )
12
+ from kodit.infrastructure.physical_architecture.detectors import docker_compose_detector
13
+
14
+ ARCHITECTURE_ENRICHMENT_SYSTEM_PROMPT = """You are an expert software architect.
15
+ Deliver the user's request succinctly.
16
+ """
17
+
18
+ ARCHITECTURE_ENRICHMENT_TASK_PROMPT = """Convert the raw architecture discovery logs
19
+ into a clean, structured summary written in markdown.
20
+
21
+ <architecture_narrative>
22
+ {architecture_narrative}
23
+ </architecture_narrative>
24
+
25
+ **Return the following information**
26
+
27
+ ## Services List
28
+
29
+ For each service, write one line:
30
+ - **[Service Name]**: [what it does] | Tech: [technology] | Ports: [ports]
31
+
32
+ ## Service Dependencies
33
+
34
+ List the important connections:
35
+ - [Service A] → [Service B]: [why they connect]
36
+
37
+ ## Mermaid Diagram
38
+
39
+ Output a Mermaid diagram depicting the architecture using the names of the services and
40
+ the ports that they expose.
41
+
42
+ ## Key Information
43
+
44
+ Answer these questions in 1-2 sentences each:
45
+ 1. What databases are used and for what?
46
+ 2. What are the critical services that everything else depends on?
47
+ 3. Are there any unusual communication patterns between services that people should be
48
+ aware of? (e.g. a different direction to what you'd expect)
49
+
50
+ ## Rules:
51
+ - Skip duplicate services (keep only one instance)
52
+ - Don't list environment variables
53
+ - Don't describe Docker volumes in detail
54
+ - Focus on WHAT each service does, not HOW it's configured
55
+ - If a service name is unclear, make your best guess based on the information
56
+ - Keep descriptions to 10 words or less per service
57
+ """
58
+
59
+
60
+ class PhysicalArchitectureService:
61
+ """Core service for discovering physical architecture and generating narrative observations.""" # noqa: E501
62
+
63
+ def __init__(self, formatter: PhysicalArchitectureFormatter) -> None:
64
+ """Initialize the service with detectors and formatter."""
65
+ self.docker_detector = docker_compose_detector.DockerComposeDetector()
66
+ self.formatter = formatter
67
+
68
+ async def discover_architecture(self, repo_path: Path) -> str:
69
+ """Discover physical architecture and generate rich narrative observations."""
70
+ # Generate repository context overview
71
+ repo_context = await self._analyze_repository_context(repo_path)
72
+
73
+ # Collect observations from all detectors
74
+ component_notes = []
75
+ connection_notes = []
76
+ infrastructure_notes = []
77
+
78
+ # Run detectors and collect narrative observations
79
+ (
80
+ docker_component_notes,
81
+ docker_connection_notes,
82
+ docker_infrastructure_notes,
83
+ ) = await self.docker_detector.analyze(repo_path)
84
+ component_notes.extend(docker_component_notes)
85
+ connection_notes.extend(docker_connection_notes)
86
+ infrastructure_notes.extend(docker_infrastructure_notes)
87
+
88
+ # Future: Add Kubernetes and code structure detectors when available
89
+
90
+ # Generate discovery metadata
91
+ discovery_metadata = self._generate_discovery_metadata(repo_path)
92
+
93
+ # Create comprehensive notes
94
+ notes = ArchitectureDiscoveryNotes(
95
+ repository_context=repo_context,
96
+ component_observations=component_notes,
97
+ connection_observations=connection_notes,
98
+ infrastructure_observations=infrastructure_notes,
99
+ discovery_metadata=discovery_metadata,
100
+ )
101
+
102
+ return self.formatter.format_for_llm(notes)
103
+
104
+ async def _analyze_repository_context(self, repo_path: Path) -> str:
105
+ """Generate high-level repository context and scope."""
106
+ context_observations = []
107
+
108
+ # Check for basic repository structure
109
+ context_observations.append(f"Analyzing repository at {repo_path}")
110
+
111
+ # Check for common project indicators
112
+ has_docker_compose = bool(
113
+ list(repo_path.glob("docker-compose*.yml"))
114
+ + list(repo_path.glob("docker-compose*.yaml"))
115
+ )
116
+ has_dockerfile = bool(list(repo_path.glob("Dockerfile*")))
117
+ has_k8s = bool(
118
+ list(repo_path.glob("**/k8s/**/*.yaml"))
119
+ + list(repo_path.glob("**/kubernetes/**/*.yaml"))
120
+ )
121
+ has_package_json = (repo_path / "package.json").exists()
122
+ has_requirements_txt = (repo_path / "requirements.txt").exists()
123
+ has_go_mod = (repo_path / "go.mod").exists()
124
+
125
+ # Determine likely project type
126
+ project_indicators = []
127
+ if has_docker_compose:
128
+ project_indicators.append("Docker Compose orchestration")
129
+ if has_dockerfile:
130
+ project_indicators.append("containerized deployment")
131
+ if has_k8s:
132
+ project_indicators.append("Kubernetes deployment")
133
+ if has_package_json:
134
+ project_indicators.append("Node.js/JavaScript components")
135
+ if has_requirements_txt:
136
+ project_indicators.append("Python components")
137
+ if has_go_mod:
138
+ project_indicators.append("Go components")
139
+
140
+ if project_indicators:
141
+ context_observations.append(
142
+ f"Repository shows evidence of {', '.join(project_indicators)}, "
143
+ "suggesting a modern containerized application architecture."
144
+ )
145
+ else:
146
+ context_observations.append(
147
+ "Repository structure analysis shows limited infrastructure configuration. " # noqa: E501
148
+ "This may be a simple application or library without complex deployment requirements." # noqa: E501
149
+ )
150
+
151
+ return " ".join(context_observations)
152
+
153
+ def _generate_discovery_metadata(self, _repo_path: Path) -> str:
154
+ """Document discovery methodology, confidence, and limitations."""
155
+ timestamp = datetime.now(UTC).isoformat()
156
+
157
+ metadata_parts = [
158
+ f"Analysis completed on {timestamp} using physical architecture discovery system version 1.0.", # noqa: E501
159
+ "Discovery methodology: Docker Compose parsing and infrastructure configuration analysis.", # noqa: E501
160
+ ]
161
+
162
+ # Document detection sources used
163
+ sources_used = ["Docker Compose file analysis"]
164
+ # Future: Add Kubernetes manifest and code analysis sources
165
+
166
+ metadata_parts.append(f"Detection sources: {', '.join(sources_used)}.")
167
+
168
+ # Document confidence levels
169
+ metadata_parts.append(
170
+ "Confidence levels: High confidence for infrastructure-defined components, "
171
+ "medium confidence for inferred roles based on naming and configuration patterns." # noqa: E501
172
+ )
173
+
174
+ # Document limitations
175
+ limitations = [
176
+ "analysis limited to Docker Compose configurations",
177
+ "code-level analysis not yet implemented",
178
+ "runtime behavior patterns not captured",
179
+ ]
180
+ metadata_parts.append(f"Current limitations: {', '.join(limitations)}.")
181
+
182
+ return " ".join(metadata_parts)
@@ -0,0 +1 @@
1
+ """Tracking domain module."""
@@ -0,0 +1,81 @@
1
+ """Domain service for resolving trackables to commits."""
2
+
3
+ import structlog
4
+
5
+ from kodit.domain.protocols import (
6
+ GitBranchRepository,
7
+ GitCommitRepository,
8
+ GitTagRepository,
9
+ )
10
+ from kodit.domain.tracking.trackable import Trackable, TrackableReferenceType
11
+
12
+
13
+ class TrackableResolutionService:
14
+ """Resolves trackables to ordered lists of commits.
15
+
16
+ This is a domain service because it orchestrates multiple aggregates
17
+ (branches, tags, commits) without belonging to any single entity.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ commit_repo: GitCommitRepository,
23
+ branch_repo: GitBranchRepository,
24
+ tag_repo: GitTagRepository,
25
+ ) -> None:
26
+ """Initialize the trackable resolution service."""
27
+ self.commit_repo = commit_repo
28
+ self.branch_repo = branch_repo
29
+ self.tag_repo = tag_repo
30
+ self.log = structlog.get_logger(__name__)
31
+
32
+ async def resolve_to_commits(
33
+ self, trackable: Trackable, limit: int = 100
34
+ ) -> list[str]:
35
+ """Resolve a trackable to an ordered list of commit SHAs.
36
+
37
+ Returns commits from newest to oldest based on git history.
38
+ """
39
+ if trackable.type == TrackableReferenceType.BRANCH:
40
+ return await self._resolve_branch(trackable, limit)
41
+ if trackable.type == TrackableReferenceType.TAG:
42
+ return await self._resolve_tag(trackable, limit)
43
+ # COMMIT_SHA
44
+ return [trackable.identifier]
45
+
46
+ async def _resolve_branch(
47
+ self, trackable: Trackable, limit: int
48
+ ) -> list[str]:
49
+ """Get commits from branch HEAD backwards through history."""
50
+ branch = await self.branch_repo.get_by_name(
51
+ trackable.identifier, trackable.repo_id
52
+ )
53
+ # Walk commit history from head_commit backwards
54
+ return await self._walk_commit_history(
55
+ branch.head_commit.commit_sha, limit
56
+ )
57
+
58
+ async def _resolve_tag(self, trackable: Trackable, limit: int) -> list[str]:
59
+ """Get commits from tag target backwards through history."""
60
+ tag = await self.tag_repo.get_by_name(
61
+ trackable.identifier, trackable.repo_id
62
+ )
63
+ return await self._walk_commit_history(
64
+ tag.target_commit.commit_sha, limit
65
+ )
66
+
67
+ async def _walk_commit_history(
68
+ self, start_sha: str, limit: int
69
+ ) -> list[str]:
70
+ """Walk commit history backwards from start_sha."""
71
+ result = []
72
+ current_sha: str | None = start_sha
73
+
74
+ for _ in range(limit):
75
+ if not current_sha:
76
+ break
77
+ result.append(current_sha)
78
+ commit = await self.commit_repo.get_by_sha(current_sha)
79
+ current_sha = commit.parent_commit_sha or None
80
+
81
+ return result
@@ -0,0 +1,21 @@
1
+ """Trackable value objects."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+
6
+
7
+ class TrackableReferenceType(StrEnum):
8
+ """Types of git references that can be tracked."""
9
+
10
+ BRANCH = "branch"
11
+ TAG = "tag"
12
+ COMMIT_SHA = "commit_sha"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Trackable:
17
+ """Represents a trackable reference point in a git repository."""
18
+
19
+ type: TrackableReferenceType
20
+ identifier: str # e.g., "main", "v1.0.0", "abc123..."
21
+ repo_id: int
@@ -346,29 +346,6 @@ class EmbeddingResponse:
346
346
  embedding: list[float]
347
347
 
348
348
 
349
- @dataclass
350
- class EnrichmentRequest:
351
- """Domain model for enrichment request."""
352
-
353
- snippet_id: str
354
- text: str
355
-
356
-
357
- @dataclass
358
- class EnrichmentResponse:
359
- """Domain model for enrichment response."""
360
-
361
- snippet_id: str
362
- text: str
363
-
364
-
365
- @dataclass
366
- class EnrichmentIndexRequest:
367
- """Domain model for enrichment index request."""
368
-
369
- requests: list[EnrichmentRequest]
370
-
371
-
372
349
  @dataclass
373
350
  class IndexView:
374
351
  """Domain model for index information."""
@@ -640,6 +617,10 @@ class TaskOperation(StrEnum):
640
617
  CREATE_CODE_EMBEDDINGS_FOR_COMMIT = "kodit.commit.create_code_embeddings"
641
618
  CREATE_SUMMARY_ENRICHMENT_FOR_COMMIT = "kodit.commit.create_summary_enrichment"
642
619
  CREATE_SUMMARY_EMBEDDINGS_FOR_COMMIT = "kodit.commit.create_summary_embeddings"
620
+ CREATE_ARCHITECTURE_ENRICHMENT_FOR_COMMIT = (
621
+ "kodit.commit.create_architecture_enrichment"
622
+ )
623
+ CREATE_PUBLIC_API_DOCS_FOR_COMMIT = "kodit.commit.create_public_api_docs"
643
624
 
644
625
  def is_repository_operation(self) -> bool:
645
626
  """Check if the task operation is a repository operation."""
@@ -663,6 +644,8 @@ class PrescribedOperations:
663
644
  TaskOperation.CREATE_CODE_EMBEDDINGS_FOR_COMMIT,
664
645
  TaskOperation.CREATE_SUMMARY_ENRICHMENT_FOR_COMMIT,
665
646
  TaskOperation.CREATE_SUMMARY_EMBEDDINGS_FOR_COMMIT,
647
+ TaskOperation.CREATE_ARCHITECTURE_ENRICHMENT_FOR_COMMIT,
648
+ TaskOperation.CREATE_PUBLIC_API_DOCS_FOR_COMMIT,
666
649
  ]
667
650
  SYNC_REPOSITORY: ClassVar[list[TaskOperation]] = [
668
651
  TaskOperation.SCAN_REPOSITORY,