context-forge-local 1.0.0__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.
Files changed (128) hide show
  1. context_forge/__init__.py +1 -0
  2. context_forge/application/__init__.py +11 -0
  3. context_forge/application/factory.py +69 -0
  4. context_forge/application/service.py +62 -0
  5. context_forge/classifier/__init__.py +1 -0
  6. context_forge/classifier/project.py +131 -0
  7. context_forge/comparison/__init__.py +29 -0
  8. context_forge/comparison/models.py +391 -0
  9. context_forge/comparison/service.py +978 -0
  10. context_forge/config/__init__.py +56 -0
  11. context_forge/config/audit.py +238 -0
  12. context_forge/config/defaults.py +10 -0
  13. context_forge/config/global_config.py +28 -0
  14. context_forge/config/loader.py +78 -0
  15. context_forge/config/loading.py +16 -0
  16. context_forge/config/project.py +139 -0
  17. context_forge/config/resolver.py +35 -0
  18. context_forge/config/validation.py +347 -0
  19. context_forge/context/__init__.py +89 -0
  20. context_forge/context/assembly.py +21 -0
  21. context_forge/context/budget_compression.py +55 -0
  22. context_forge/context/candidate.py +26 -0
  23. context_forge/context/candidates.py +441 -0
  24. context_forge/context/compression.py +9 -0
  25. context_forge/context/compression_models.py +23 -0
  26. context_forge/context/compression_pipeline.py +25 -0
  27. context_forge/context/depth.py +79 -0
  28. context_forge/context/deterministic_compression.py +132 -0
  29. context_forge/context/engine.py +107 -0
  30. context_forge/context/enrichment.py +14 -0
  31. context_forge/context/enrichment_pipeline.py +19 -0
  32. context_forge/context/expansion.py +133 -0
  33. context_forge/context/file_enrichment.py +70 -0
  34. context_forge/context/git_relevance.py +31 -0
  35. context_forge/context/models.py +56 -0
  36. context_forge/context/package.py +164 -0
  37. context_forge/context/prioritization.py +50 -0
  38. context_forge/context/priority.py +11 -0
  39. context_forge/context/priority_ordering.py +34 -0
  40. context_forge/context/ranking.py +56 -0
  41. context_forge/context/relationship_enrichment.py +43 -0
  42. context_forge/context/request.py +12 -0
  43. context_forge/context/retrieval.py +241 -0
  44. context_forge/context/selection.py +44 -0
  45. context_forge/context/serialization.py +67 -0
  46. context_forge/context/signals.py +21 -0
  47. context_forge/context/symbol_enrichment.py +82 -0
  48. context_forge/context/types.py +7 -0
  49. context_forge/evaluation/__init__.py +67 -0
  50. context_forge/evaluation/baseline.py +188 -0
  51. context_forge/evaluation/end_to_end.py +85 -0
  52. context_forge/evaluation/ground_truth.py +47 -0
  53. context_forge/evaluation/metrics.py +32 -0
  54. context_forge/evaluation/models.py +46 -0
  55. context_forge/evaluation/regression.py +103 -0
  56. context_forge/evaluation/repositories.py +216 -0
  57. context_forge/evaluation/retrieval.py +139 -0
  58. context_forge/evaluation/selection.py +102 -0
  59. context_forge/evaluation/structural.py +156 -0
  60. context_forge/extractor/__init__.py +1 -0
  61. context_forge/git/__init__.py +35 -0
  62. context_forge/git/intelligence.py +28 -0
  63. context_forge/git/models.py +137 -0
  64. context_forge/git/navigation.py +560 -0
  65. context_forge/git/repository.py +484 -0
  66. context_forge/graph/__init__.py +1 -0
  67. context_forge/graph/builder.py +565 -0
  68. context_forge/inspection/__init__.py +13 -0
  69. context_forge/inspection/models.py +103 -0
  70. context_forge/inspection/workflow.py +679 -0
  71. context_forge/main.py +4272 -0
  72. context_forge/models/__init__.py +1 -0
  73. context_forge/models/directory.py +20 -0
  74. context_forge/models/enums.py +25 -0
  75. context_forge/models/file.py +24 -0
  76. context_forge/models/project.py +92 -0
  77. context_forge/models/relationship.py +30 -0
  78. context_forge/models/symbol.py +34 -0
  79. context_forge/operational_logging.py +107 -0
  80. context_forge/parser/__init__.py +15 -0
  81. context_forge/parser/base.py +16 -0
  82. context_forge/parser/detector.py +26 -0
  83. context_forge/parser/language.py +13 -0
  84. context_forge/parser/python.py +316 -0
  85. context_forge/parser/registry.py +24 -0
  86. context_forge/parser/result.py +56 -0
  87. context_forge/pipeline/__init__.py +13 -0
  88. context_forge/pipeline/analyzer.py +309 -0
  89. context_forge/pipeline/freshness.py +643 -0
  90. context_forge/preflight/__init__.py +11 -0
  91. context_forge/preflight/readiness.py +519 -0
  92. context_forge/query/__init__.py +194 -0
  93. context_forge/query/architecture.py +70 -0
  94. context_forge/query/architecture_report.py +417 -0
  95. context_forge/query/changeset.py +820 -0
  96. context_forge/query/health.py +318 -0
  97. context_forge/query/impact.py +1103 -0
  98. context_forge/query/locator.py +663 -0
  99. context_forge/query/package_boundary.py +519 -0
  100. context_forge/query/path.py +532 -0
  101. context_forge/query/post_change.py +641 -0
  102. context_forge/query/project.py +1515 -0
  103. context_forge/query/regression_rules.py +738 -0
  104. context_forge/query/result.py +20 -0
  105. context_forge/query/risk.py +862 -0
  106. context_forge/query/unused.py +1012 -0
  107. context_forge/query/verification.py +900 -0
  108. context_forge/reporting/__init__.py +37 -0
  109. context_forge/reporting/contract.py +396 -0
  110. context_forge/reporting/errors.py +223 -0
  111. context_forge/reporting/formatter.py +68 -0
  112. context_forge/scanner/__init__.py +1 -0
  113. context_forge/scanner/repository.py +278 -0
  114. context_forge/storage/__init__.py +1 -0
  115. context_forge/storage/cache.py +192 -0
  116. context_forge/storage/database.py +180 -0
  117. context_forge/storage/repository.py +1009 -0
  118. context_forge/task/__init__.py +31 -0
  119. context_forge/task/grounding.py +188 -0
  120. context_forge/task/models.py +92 -0
  121. context_forge/task/repository_grounding.py +95 -0
  122. context_forge/task/service.py +99 -0
  123. context_forge/task/validation.py +56 -0
  124. context_forge_local-1.0.0.dist-info/METADATA +61 -0
  125. context_forge_local-1.0.0.dist-info/RECORD +128 -0
  126. context_forge_local-1.0.0.dist-info/WHEEL +4 -0
  127. context_forge_local-1.0.0.dist-info/entry_points.txt +3 -0
  128. context_forge_local-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,11 @@
1
+ from context_forge.application.factory import (
2
+ build_context_engine,
3
+ build_context_service,
4
+ )
5
+ from context_forge.application.service import RepositoryContextService
6
+
7
+ __all__ = [
8
+ "RepositoryContextService",
9
+ "build_context_engine",
10
+ "build_context_service",
11
+ ]
@@ -0,0 +1,69 @@
1
+ from context_forge.application.service import RepositoryContextService
2
+ from context_forge.context import (
3
+ CandidateGenerator,
4
+ ContextAssembler,
5
+ ContextBudgetCompressor,
6
+ ContextCompressionPipeline,
7
+ ContextEnrichmentPipeline,
8
+ ContextPackageBuilder,
9
+ ContextPriorityOrdering,
10
+ ContextSelector,
11
+ DefaultContextEngine,
12
+ DeterministicContextCompressor,
13
+ DeterministicPrioritizer,
14
+ DeterministicRanker,
15
+ FileContextEnricher,
16
+ GraphExpander,
17
+ RelationshipContextEnricher,
18
+ SymbolContextEnricher,
19
+ )
20
+ from context_forge.context.depth import ContextDepthSelector
21
+ from context_forge.task import (
22
+ TaskGroundingService,
23
+ TaskRepositoryGroundingService,
24
+ TaskUnderstandingService,
25
+ TaskValidator,
26
+ )
27
+
28
+
29
+ def build_context_engine(
30
+ max_context_units: int = 20,
31
+ ) -> DefaultContextEngine:
32
+ return DefaultContextEngine(
33
+ candidate_generator=CandidateGenerator(),
34
+ ranker=DeterministicRanker(),
35
+ selector=ContextSelector(),
36
+ depth_selector=ContextDepthSelector(),
37
+ expander=GraphExpander(),
38
+ package_builder=ContextPackageBuilder(),
39
+ enrichment_pipeline=ContextEnrichmentPipeline(
40
+ enrichers=[
41
+ FileContextEnricher(),
42
+ SymbolContextEnricher(),
43
+ RelationshipContextEnricher(),
44
+ ],
45
+ ),
46
+ compression_pipeline=ContextCompressionPipeline(
47
+ compressor=DeterministicContextCompressor(),
48
+ budget_compressor=ContextBudgetCompressor(),
49
+ ),
50
+ assembly=ContextAssembler(
51
+ ContextPriorityOrdering(
52
+ DeterministicPrioritizer(),
53
+ ),
54
+ ),
55
+ max_context_units=max_context_units,
56
+ )
57
+
58
+
59
+ def build_context_service(
60
+ max_context_units: int = 20,
61
+ ) -> RepositoryContextService:
62
+ engine = build_context_engine(max_context_units=max_context_units)
63
+ return RepositoryContextService(
64
+ engine=engine,
65
+ task_understanding=TaskUnderstandingService(),
66
+ task_validator=TaskValidator(),
67
+ task_grounding=TaskGroundingService(),
68
+ task_repository_grounding=TaskRepositoryGroundingService(),
69
+ )
@@ -0,0 +1,62 @@
1
+ from context_forge.context.engine import ContextEngine
2
+ from context_forge.context.models import ContextPackage
3
+ from context_forge.context.request import ContextRequest
4
+ from context_forge.models.project import Project
5
+ from context_forge.task import (
6
+ TaskGroundingService,
7
+ TaskRepositoryGroundingService,
8
+ TaskState,
9
+ TaskUnderstandingService,
10
+ TaskValidator,
11
+ )
12
+
13
+
14
+ class RepositoryContextService:
15
+ def __init__(
16
+ self,
17
+ engine: ContextEngine,
18
+ task_understanding: TaskUnderstandingService | None = None,
19
+ task_validator: TaskValidator | None = None,
20
+ task_grounding: TaskGroundingService | None = None,
21
+ task_repository_grounding: TaskRepositoryGroundingService | None = None,
22
+ ) -> None:
23
+ self.engine = engine
24
+ self.task_understanding = task_understanding or TaskUnderstandingService()
25
+ self.task_validator = task_validator or TaskValidator()
26
+ self.task_grounding = task_grounding or TaskGroundingService()
27
+ self.task_repository_grounding = (
28
+ task_repository_grounding or TaskRepositoryGroundingService()
29
+ )
30
+
31
+ def retrieve_context(
32
+ self,
33
+ project: Project,
34
+ task: str,
35
+ ) -> ContextPackage:
36
+ if not task.strip():
37
+ raise ValueError("task must not be empty")
38
+
39
+ interpretation = self.task_understanding.understand(task)
40
+ validation = self.task_validator.validate(interpretation)
41
+
42
+ if validation.state != TaskState.CLEAR:
43
+ raise ValueError(f"task validation failed: {validation.state.value}")
44
+
45
+ grounded_task = self.task_grounding.ground(
46
+ project,
47
+ interpretation,
48
+ )
49
+
50
+ grounding = self.task_repository_grounding.ground(
51
+ project,
52
+ grounded_task,
53
+ )
54
+
55
+ return self.engine.build(
56
+ ContextRequest(
57
+ project=project,
58
+ task=task,
59
+ interpretation=interpretation,
60
+ grounding=grounding,
61
+ )
62
+ )
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,131 @@
1
+ from context_forge.models.enums import FileType
2
+ from context_forge.models.project import Project
3
+
4
+ LANGUAGE_BY_EXTENSION = {
5
+ ".py": "Python",
6
+ ".js": "JavaScript",
7
+ ".jsx": "JavaScript",
8
+ ".ts": "TypeScript",
9
+ ".tsx": "TypeScript",
10
+ ".java": "Java",
11
+ ".cpp": "C++",
12
+ ".cc": "C++",
13
+ ".hpp": "C++",
14
+ ".c": "C",
15
+ ".h": "C",
16
+ ".go": "Go",
17
+ ".rs": "Rust",
18
+ ".rb": "Ruby",
19
+ ".swift": "Swift",
20
+ ".kt": "Kotlin",
21
+ ".php": "PHP",
22
+ }
23
+
24
+ DOCUMENTATION_EXTENSIONS = {".md", ".rst", ".txt"}
25
+
26
+ CONFIGURATION_FILES = {
27
+ "pyproject.toml",
28
+ "package.json",
29
+ "package-lock.json",
30
+ "uv.lock",
31
+ "Cargo.toml",
32
+ "go.mod",
33
+ "requirements.txt",
34
+ "Makefile",
35
+ ".gitignore",
36
+ }
37
+
38
+ FRAMEWORK_INDICATORS = {
39
+ "fastapi": "FASTAPI",
40
+ "django": "Django",
41
+ "flask": "Flask",
42
+ "react": "React",
43
+ "next": "Next.js",
44
+ }
45
+
46
+
47
+ class ProjectClassifier:
48
+ def classify(self, project: Project) -> Project:
49
+ self._classify_files(project)
50
+ self._detect_languages(project)
51
+ self._detect_project_types(project)
52
+ self._detect_package_manager(project)
53
+ self._detect_frameworks(project)
54
+
55
+ return project
56
+
57
+ def _detect_project_types(self, project: Project) -> None:
58
+ file_names = {file.name for file in project.files}
59
+
60
+ if "pyproject.toml" in file_names:
61
+ project.project_type = "python_project"
62
+ elif "package.json" in file_names:
63
+ project.project_type = "javascript_project"
64
+ elif "Cargo.toml" in file_names:
65
+ project.project_type = "rust_project"
66
+ elif "go.mod" in file_names:
67
+ project.project_type = "go_project"
68
+ elif "pom.xml" in file_names:
69
+ project.project_type = "java_project"
70
+ else:
71
+ project.project_type = "unknown"
72
+
73
+ def _classify_files(self, project: Project) -> None:
74
+ for file in project.files:
75
+ if file.name in CONFIGURATION_FILES:
76
+ file.file_type = FileType.CONFIGURATION
77
+ elif file.extension in DOCUMENTATION_EXTENSIONS:
78
+ file.file_type = FileType.DOCUMENTATION
79
+ elif file.path.parts and "tests" in file.path.parts:
80
+ file.file_type = FileType.TEST
81
+ elif file.extension in LANGUAGE_BY_EXTENSION:
82
+ file.file_type = FileType.SOURCE
83
+ else:
84
+ file.file_type = FileType.UNKNOWN
85
+
86
+ def _detect_languages(self, project: Project):
87
+ languages = {
88
+ LANGUAGE_BY_EXTENSION[file.extension]
89
+ for file in project.files
90
+ if file.extension in LANGUAGE_BY_EXTENSION
91
+ }
92
+
93
+ project.languages = sorted(languages)
94
+
95
+ def _detect_package_manager(self, project: Project) -> None:
96
+ file_names = {file.name for file in project.files}
97
+
98
+ if "uv.lock" in file_names:
99
+ project.package_manager = "uv"
100
+ elif "requirements.txt" in file_names:
101
+ project.package_manager = "pip"
102
+ elif "package-lock.json" in file_names:
103
+ project.package_manager = "npm"
104
+ elif "pnpm-lock.yaml" in file_names:
105
+ project.package_manager = "pnpm"
106
+ elif "yarn.lock" in file_names:
107
+ project.package_manager = "yarn"
108
+ elif "Cargo.toml" in file_names:
109
+ project.package_manager = "cargo"
110
+ elif "go.mod" in file_names:
111
+ project.package_manager = "go"
112
+
113
+ def _detect_frameworks(self, project: Project) -> None:
114
+ detected: set[str] = set()
115
+
116
+ for file in project.files:
117
+ if file.extension != ".py":
118
+ continue
119
+
120
+ try:
121
+ content = (project.root_path / file.path).read_text(encoding="utf-8")
122
+ except (OSError, UnicodeDecodeError):
123
+ continue
124
+
125
+ content_lower = content.lower()
126
+
127
+ for indicator, framework in FRAMEWORK_INDICATORS.items():
128
+ if f"import {indicator}" in content_lower:
129
+ detected.add(framework)
130
+
131
+ project.frameworks = sorted(detected)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from context_forge.comparison.models import (
4
+ AffectedComponent,
5
+ AmbiguousMatch,
6
+ ComparisonState,
7
+ FileDiff,
8
+ RelationshipDiff,
9
+ StateValidationResult,
10
+ StructuralComparisonResult,
11
+ SymbolDiff,
12
+ )
13
+ from context_forge.comparison.service import (
14
+ compare_states,
15
+ validate_comparison_states,
16
+ )
17
+
18
+ __all__ = [
19
+ "AffectedComponent",
20
+ "AmbiguousMatch",
21
+ "ComparisonState",
22
+ "FileDiff",
23
+ "RelationshipDiff",
24
+ "StateValidationResult",
25
+ "StructuralComparisonResult",
26
+ "SymbolDiff",
27
+ "compare_states",
28
+ "validate_comparison_states",
29
+ ]
@@ -0,0 +1,391 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from context_forge.models.project import Project
8
+ from context_forge.pipeline.freshness import FreshnessState, WorkflowFreshness
9
+ from context_forge.reporting.contract import (
10
+ DiagnosticItem,
11
+ OperationOutcome,
12
+ )
13
+ from context_forge.storage.cache import FileFingerprint, RepositoryCacheMetadata
14
+
15
+ COMPLETE_ANALYSIS_STATUSES: frozenset[str] = frozenset({"analyzed", "completed"})
16
+
17
+
18
+ def is_terminal_complete_status(status: str | None) -> bool:
19
+ if not status or not isinstance(status, str):
20
+ return False
21
+ return status.strip().lower() in COMPLETE_ANALYSIS_STATUSES
22
+
23
+
24
+ @dataclass
25
+ class ComparisonState:
26
+ project: Project | None = None
27
+ state_id: str | None = None
28
+ repository_path: Path | str | None = None
29
+ metadata: RepositoryCacheMetadata | None = None
30
+ fingerprints: dict[Path, FileFingerprint | Any] | None = None
31
+ analysis_status: str | None = None
32
+ analysis_errors: tuple[str, ...] = ()
33
+ freshness: FreshnessState | WorkflowFreshness | None = None
34
+ is_historical: bool = False
35
+ source_label: str | None = None
36
+
37
+ def __post_init__(self) -> None:
38
+ if self.repository_path is not None:
39
+ self.repository_path = Path(self.repository_path)
40
+ elif self.project is not None:
41
+ self.repository_path = self.project.root_path
42
+
43
+ if self.state_id is None:
44
+ if self.project is not None:
45
+ self.state_id = str(self.project.id)
46
+ elif self.metadata is not None:
47
+ self.state_id = str(self.metadata.project_id)
48
+
49
+ if not self.analysis_status and self.project is not None:
50
+ self.analysis_status = self.project.analysis_status
51
+
52
+ if (
53
+ not self.analysis_errors
54
+ and self.project is not None
55
+ and self.project.errors
56
+ ):
57
+ self.analysis_errors = tuple(self.project.errors)
58
+ elif isinstance(self.analysis_errors, list):
59
+ self.analysis_errors = tuple(self.analysis_errors)
60
+
61
+ @property
62
+ def is_supplied(self) -> bool:
63
+ return self.project is not None
64
+
65
+ @property
66
+ def is_usable(self) -> bool:
67
+ if self.project is None:
68
+ return False
69
+ return bool(self.state_id)
70
+
71
+ @property
72
+ def repository_key(self) -> str:
73
+ if self.metadata is not None and self.metadata.repository_key:
74
+ return self.metadata.repository_key
75
+ if self.repository_path is not None:
76
+ return str(self.repository_path.resolve())
77
+ return ""
78
+
79
+ @property
80
+ def is_complete(self) -> bool:
81
+ if not self.is_supplied:
82
+ return False
83
+ if not is_terminal_complete_status(self.analysis_status):
84
+ return False
85
+ if len(self.analysis_errors) > 0:
86
+ return False
87
+ if self.freshness is not None:
88
+ state_freshness = (
89
+ self.freshness.state
90
+ if isinstance(self.freshness, WorkflowFreshness)
91
+ else self.freshness
92
+ )
93
+ if state_freshness == FreshnessState.INCOMPLETE:
94
+ return False
95
+ if state_freshness == FreshnessState.INDETERMINATE:
96
+ return False
97
+ if state_freshness == FreshnessState.STALE and not self.is_historical:
98
+ return False
99
+ return True
100
+
101
+ @classmethod
102
+ def from_project(
103
+ cls,
104
+ project: Project,
105
+ *,
106
+ state_id: str | None = None,
107
+ metadata: RepositoryCacheMetadata | None = None,
108
+ fingerprints: dict[Path, FileFingerprint | Any] | None = None,
109
+ freshness: FreshnessState | WorkflowFreshness | None = None,
110
+ is_historical: bool = False,
111
+ source_label: str | None = None,
112
+ ) -> ComparisonState:
113
+ return cls(
114
+ project=project,
115
+ state_id=state_id or str(project.id),
116
+ repository_path=project.root_path,
117
+ metadata=metadata,
118
+ fingerprints=fingerprints,
119
+ analysis_status=project.analysis_status,
120
+ analysis_errors=tuple(project.errors),
121
+ freshness=freshness,
122
+ is_historical=is_historical,
123
+ source_label=source_label,
124
+ )
125
+
126
+ def to_dict(self) -> dict[str, Any]:
127
+ result: dict[str, Any] = {
128
+ "is_historical": self.is_historical,
129
+ "is_supplied": self.is_supplied,
130
+ "is_usable": self.is_usable,
131
+ "is_complete": self.is_complete,
132
+ "repository_key": self.repository_key,
133
+ "state_id": self.state_id,
134
+ }
135
+ if self.source_label:
136
+ result["source_label"] = self.source_label
137
+ if self.analysis_status:
138
+ result["analysis_status"] = self.analysis_status
139
+ if self.analysis_errors:
140
+ result["analysis_errors"] = list(self.analysis_errors)
141
+ if self.freshness is not None:
142
+ freshness_val = (
143
+ self.freshness.state.value
144
+ if isinstance(self.freshness, WorkflowFreshness)
145
+ else (
146
+ self.freshness.value
147
+ if isinstance(self.freshness, FreshnessState)
148
+ else str(self.freshness)
149
+ )
150
+ )
151
+ result["freshness"] = freshness_val
152
+ if self.project is not None:
153
+ result["files_count"] = len(self.project.files)
154
+ result["symbols_count"] = len(self.project.symbols)
155
+ result["relationships_count"] = len(self.project.relationships)
156
+ return result
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class StateValidationResult:
161
+ is_valid: bool
162
+ outcome: OperationOutcome
163
+ before_state: ComparisonState | None = None
164
+ after_state: ComparisonState | None = None
165
+ is_complete: bool = True
166
+ reason: str = ""
167
+
168
+ def to_dict(self) -> dict[str, Any]:
169
+ return {
170
+ "is_complete": self.is_complete,
171
+ "is_valid": self.is_valid,
172
+ "outcome": self.outcome.to_dict(),
173
+ "reason": self.reason,
174
+ }
175
+
176
+
177
+ @dataclass(frozen=True)
178
+ class FileDiff:
179
+ path: str
180
+ change_type: str
181
+ before_path: str | None = None
182
+ after_path: str | None = None
183
+ before_fingerprint: str | None = None
184
+ after_fingerprint: str | None = None
185
+ details: dict[str, Any] = field(default_factory=dict)
186
+
187
+ def to_dict(self) -> dict[str, Any]:
188
+ payload: dict[str, Any] = {
189
+ "change_type": self.change_type,
190
+ "path": self.path,
191
+ }
192
+ if self.before_path:
193
+ payload["before_path"] = self.before_path
194
+ if self.after_path:
195
+ payload["after_path"] = self.after_path
196
+ if self.before_fingerprint:
197
+ payload["before_fingerprint"] = self.before_fingerprint
198
+ if self.after_fingerprint:
199
+ payload["after_fingerprint"] = self.after_fingerprint
200
+ if self.details:
201
+ payload["details"] = dict(sorted(self.details.items()))
202
+ return payload
203
+
204
+
205
+ @dataclass(frozen=True)
206
+ class SymbolDiff:
207
+ name: str
208
+ kind: str
209
+ file_path: str
210
+ change_type: str
211
+ qualified_name: str | None = None
212
+ before_file_path: str | None = None
213
+ after_file_path: str | None = None
214
+ before_signature: str | None = None
215
+ after_signature: str | None = None
216
+ before_lines: tuple[int, int] | None = None
217
+ after_lines: tuple[int, int] | None = None
218
+ is_line_shift_only: bool = False
219
+ details: dict[str, Any] = field(default_factory=dict)
220
+
221
+ def to_dict(self) -> dict[str, Any]:
222
+ payload: dict[str, Any] = {
223
+ "change_type": self.change_type,
224
+ "file_path": self.file_path,
225
+ "kind": self.kind,
226
+ "name": self.name,
227
+ }
228
+ if self.qualified_name:
229
+ payload["qualified_name"] = self.qualified_name
230
+ if self.before_file_path:
231
+ payload["before_file_path"] = self.before_file_path
232
+ if self.after_file_path:
233
+ payload["after_file_path"] = self.after_file_path
234
+ if self.before_signature:
235
+ payload["before_signature"] = self.before_signature
236
+ if self.after_signature:
237
+ payload["after_signature"] = self.after_signature
238
+ if self.before_lines:
239
+ payload["before_lines"] = list(self.before_lines)
240
+ if self.after_lines:
241
+ payload["after_lines"] = list(self.after_lines)
242
+ if self.is_line_shift_only:
243
+ payload["is_line_shift_only"] = self.is_line_shift_only
244
+ if self.details:
245
+ payload["details"] = dict(sorted(self.details.items()))
246
+ return payload
247
+
248
+
249
+ @dataclass(frozen=True)
250
+ class RelationshipDiff:
251
+ source_key: str
252
+ target_key: str
253
+ relationship_type: str
254
+ change_type: str
255
+ before_confidence: float | None = None
256
+ after_confidence: float | None = None
257
+ metadata_changes: dict[str, Any] = field(default_factory=dict)
258
+ details: dict[str, Any] = field(default_factory=dict)
259
+
260
+ def to_dict(self) -> dict[str, Any]:
261
+ payload: dict[str, Any] = {
262
+ "change_type": self.change_type,
263
+ "relationship_type": self.relationship_type,
264
+ "source_key": self.source_key,
265
+ "target_key": self.target_key,
266
+ }
267
+ if self.before_confidence is not None:
268
+ payload["before_confidence"] = self.before_confidence
269
+ if self.after_confidence is not None:
270
+ payload["after_confidence"] = self.after_confidence
271
+ if self.metadata_changes:
272
+ payload["metadata_changes"] = dict(sorted(self.metadata_changes.items()))
273
+ if self.details:
274
+ payload["details"] = dict(sorted(self.details.items()))
275
+ return payload
276
+
277
+
278
+ @dataclass(frozen=True)
279
+ class AmbiguousMatch:
280
+ category: str
281
+ before_candidates: tuple[str, ...]
282
+ after_candidates: tuple[str, ...]
283
+ reason: str
284
+
285
+ def to_dict(self) -> dict[str, Any]:
286
+ return {
287
+ "after_candidates": list(self.after_candidates),
288
+ "before_candidates": list(self.before_candidates),
289
+ "category": self.category,
290
+ "reason": self.reason,
291
+ }
292
+
293
+
294
+ @dataclass(frozen=True)
295
+ class AffectedComponent:
296
+ entity_key: str
297
+ entity_type: str
298
+ path: str
299
+ reason: str
300
+
301
+ def to_dict(self) -> dict[str, Any]:
302
+ return {
303
+ "entity_key": self.entity_key,
304
+ "entity_type": self.entity_type,
305
+ "path": self.path,
306
+ "reason": self.reason,
307
+ }
308
+
309
+
310
+ @dataclass(frozen=True)
311
+ class StructuralComparisonResult:
312
+ before_state_id: str | None
313
+ after_state_id: str | None
314
+ repository_key: str
315
+ outcome: OperationOutcome
316
+ is_complete: bool = True
317
+ added_files: tuple[FileDiff, ...] = ()
318
+ removed_files: tuple[FileDiff, ...] = ()
319
+ modified_files: tuple[FileDiff, ...] = ()
320
+ moved_files: tuple[FileDiff, ...] = ()
321
+ added_symbols: tuple[SymbolDiff, ...] = ()
322
+ removed_symbols: tuple[SymbolDiff, ...] = ()
323
+ modified_symbols: tuple[SymbolDiff, ...] = ()
324
+ moved_symbols: tuple[SymbolDiff, ...] = ()
325
+ added_relationships: tuple[RelationshipDiff, ...] = ()
326
+ removed_relationships: tuple[RelationshipDiff, ...] = ()
327
+ modified_relationships: tuple[RelationshipDiff, ...] = ()
328
+ ambiguous_matches: tuple[AmbiguousMatch, ...] = ()
329
+ affected_components: tuple[AffectedComponent, ...] = ()
330
+ diagnostics: tuple[DiagnosticItem, ...] = ()
331
+ limitations: tuple[str, ...] = ()
332
+
333
+ @property
334
+ def has_structural_changes(self) -> bool:
335
+ return bool(
336
+ self.added_files
337
+ or self.removed_files
338
+ or self.modified_files
339
+ or self.moved_files
340
+ or self.added_symbols
341
+ or self.removed_symbols
342
+ or self.modified_symbols
343
+ or self.moved_symbols
344
+ or self.added_relationships
345
+ or self.removed_relationships
346
+ or self.modified_relationships
347
+ )
348
+
349
+ @property
350
+ def total_changes_count(self) -> int:
351
+ return (
352
+ len(self.added_files)
353
+ + len(self.removed_files)
354
+ + len(self.modified_files)
355
+ + len(self.moved_files)
356
+ + len(self.added_symbols)
357
+ + len(self.removed_symbols)
358
+ + len(self.modified_symbols)
359
+ + len(self.moved_symbols)
360
+ + len(self.added_relationships)
361
+ + len(self.removed_relationships)
362
+ + len(self.modified_relationships)
363
+ )
364
+
365
+ def to_dict(self) -> dict[str, Any]:
366
+ return {
367
+ "added_files": [f.to_dict() for f in self.added_files],
368
+ "added_relationships": [r.to_dict() for r in self.added_relationships],
369
+ "added_symbols": [s.to_dict() for s in self.added_symbols],
370
+ "affected_components": [a.to_dict() for a in self.affected_components],
371
+ "after_state_id": self.after_state_id,
372
+ "ambiguous_matches": [m.to_dict() for m in self.ambiguous_matches],
373
+ "before_state_id": self.before_state_id,
374
+ "diagnostics": [d.to_dict() for d in self.diagnostics],
375
+ "has_structural_changes": self.has_structural_changes,
376
+ "is_complete": self.is_complete,
377
+ "limitations": list(self.limitations),
378
+ "modified_files": [f.to_dict() for f in self.modified_files],
379
+ "modified_relationships": [
380
+ r.to_dict() for r in self.modified_relationships
381
+ ],
382
+ "modified_symbols": [s.to_dict() for s in self.modified_symbols],
383
+ "moved_files": [f.to_dict() for f in self.moved_files],
384
+ "moved_symbols": [s.to_dict() for s in self.moved_symbols],
385
+ "outcome": self.outcome.to_dict(),
386
+ "removed_files": [f.to_dict() for f in self.removed_files],
387
+ "removed_relationships": [r.to_dict() for r in self.removed_relationships],
388
+ "removed_symbols": [s.to_dict() for s in self.removed_symbols],
389
+ "repository_key": self.repository_key,
390
+ "total_changes_count": self.total_changes_count,
391
+ }