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.
- context_forge/__init__.py +1 -0
- context_forge/application/__init__.py +11 -0
- context_forge/application/factory.py +69 -0
- context_forge/application/service.py +62 -0
- context_forge/classifier/__init__.py +1 -0
- context_forge/classifier/project.py +131 -0
- context_forge/comparison/__init__.py +29 -0
- context_forge/comparison/models.py +391 -0
- context_forge/comparison/service.py +978 -0
- context_forge/config/__init__.py +56 -0
- context_forge/config/audit.py +238 -0
- context_forge/config/defaults.py +10 -0
- context_forge/config/global_config.py +28 -0
- context_forge/config/loader.py +78 -0
- context_forge/config/loading.py +16 -0
- context_forge/config/project.py +139 -0
- context_forge/config/resolver.py +35 -0
- context_forge/config/validation.py +347 -0
- context_forge/context/__init__.py +89 -0
- context_forge/context/assembly.py +21 -0
- context_forge/context/budget_compression.py +55 -0
- context_forge/context/candidate.py +26 -0
- context_forge/context/candidates.py +441 -0
- context_forge/context/compression.py +9 -0
- context_forge/context/compression_models.py +23 -0
- context_forge/context/compression_pipeline.py +25 -0
- context_forge/context/depth.py +79 -0
- context_forge/context/deterministic_compression.py +132 -0
- context_forge/context/engine.py +107 -0
- context_forge/context/enrichment.py +14 -0
- context_forge/context/enrichment_pipeline.py +19 -0
- context_forge/context/expansion.py +133 -0
- context_forge/context/file_enrichment.py +70 -0
- context_forge/context/git_relevance.py +31 -0
- context_forge/context/models.py +56 -0
- context_forge/context/package.py +164 -0
- context_forge/context/prioritization.py +50 -0
- context_forge/context/priority.py +11 -0
- context_forge/context/priority_ordering.py +34 -0
- context_forge/context/ranking.py +56 -0
- context_forge/context/relationship_enrichment.py +43 -0
- context_forge/context/request.py +12 -0
- context_forge/context/retrieval.py +241 -0
- context_forge/context/selection.py +44 -0
- context_forge/context/serialization.py +67 -0
- context_forge/context/signals.py +21 -0
- context_forge/context/symbol_enrichment.py +82 -0
- context_forge/context/types.py +7 -0
- context_forge/evaluation/__init__.py +67 -0
- context_forge/evaluation/baseline.py +188 -0
- context_forge/evaluation/end_to_end.py +85 -0
- context_forge/evaluation/ground_truth.py +47 -0
- context_forge/evaluation/metrics.py +32 -0
- context_forge/evaluation/models.py +46 -0
- context_forge/evaluation/regression.py +103 -0
- context_forge/evaluation/repositories.py +216 -0
- context_forge/evaluation/retrieval.py +139 -0
- context_forge/evaluation/selection.py +102 -0
- context_forge/evaluation/structural.py +156 -0
- context_forge/extractor/__init__.py +1 -0
- context_forge/git/__init__.py +35 -0
- context_forge/git/intelligence.py +28 -0
- context_forge/git/models.py +137 -0
- context_forge/git/navigation.py +560 -0
- context_forge/git/repository.py +484 -0
- context_forge/graph/__init__.py +1 -0
- context_forge/graph/builder.py +565 -0
- context_forge/inspection/__init__.py +13 -0
- context_forge/inspection/models.py +103 -0
- context_forge/inspection/workflow.py +679 -0
- context_forge/main.py +4272 -0
- context_forge/models/__init__.py +1 -0
- context_forge/models/directory.py +20 -0
- context_forge/models/enums.py +25 -0
- context_forge/models/file.py +24 -0
- context_forge/models/project.py +92 -0
- context_forge/models/relationship.py +30 -0
- context_forge/models/symbol.py +34 -0
- context_forge/operational_logging.py +107 -0
- context_forge/parser/__init__.py +15 -0
- context_forge/parser/base.py +16 -0
- context_forge/parser/detector.py +26 -0
- context_forge/parser/language.py +13 -0
- context_forge/parser/python.py +316 -0
- context_forge/parser/registry.py +24 -0
- context_forge/parser/result.py +56 -0
- context_forge/pipeline/__init__.py +13 -0
- context_forge/pipeline/analyzer.py +309 -0
- context_forge/pipeline/freshness.py +643 -0
- context_forge/preflight/__init__.py +11 -0
- context_forge/preflight/readiness.py +519 -0
- context_forge/query/__init__.py +194 -0
- context_forge/query/architecture.py +70 -0
- context_forge/query/architecture_report.py +417 -0
- context_forge/query/changeset.py +820 -0
- context_forge/query/health.py +318 -0
- context_forge/query/impact.py +1103 -0
- context_forge/query/locator.py +663 -0
- context_forge/query/package_boundary.py +519 -0
- context_forge/query/path.py +532 -0
- context_forge/query/post_change.py +641 -0
- context_forge/query/project.py +1515 -0
- context_forge/query/regression_rules.py +738 -0
- context_forge/query/result.py +20 -0
- context_forge/query/risk.py +862 -0
- context_forge/query/unused.py +1012 -0
- context_forge/query/verification.py +900 -0
- context_forge/reporting/__init__.py +37 -0
- context_forge/reporting/contract.py +396 -0
- context_forge/reporting/errors.py +223 -0
- context_forge/reporting/formatter.py +68 -0
- context_forge/scanner/__init__.py +1 -0
- context_forge/scanner/repository.py +278 -0
- context_forge/storage/__init__.py +1 -0
- context_forge/storage/cache.py +192 -0
- context_forge/storage/database.py +180 -0
- context_forge/storage/repository.py +1009 -0
- context_forge/task/__init__.py +31 -0
- context_forge/task/grounding.py +188 -0
- context_forge/task/models.py +92 -0
- context_forge/task/repository_grounding.py +95 -0
- context_forge/task/service.py +99 -0
- context_forge/task/validation.py +56 -0
- context_forge_local-1.0.0.dist-info/METADATA +61 -0
- context_forge_local-1.0.0.dist-info/RECORD +128 -0
- context_forge_local-1.0.0.dist-info/WHEEL +4 -0
- context_forge_local-1.0.0.dist-info/entry_points.txt +3 -0
- 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
|
+
}
|