archapi 0.3.0__tar.gz

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 (38) hide show
  1. archapi-0.3.0/LICENSE +21 -0
  2. archapi-0.3.0/PKG-INFO +79 -0
  3. archapi-0.3.0/README.md +56 -0
  4. archapi-0.3.0/archapi/__init__.py +3 -0
  5. archapi-0.3.0/archapi/core.py +295 -0
  6. archapi-0.3.0/archapi/frameworks/__init__.py +0 -0
  7. archapi-0.3.0/archapi/frameworks/base.py +56 -0
  8. archapi-0.3.0/archapi/frameworks/detector.py +113 -0
  9. archapi-0.3.0/archapi/frameworks/express_ts/__init__.py +0 -0
  10. archapi-0.3.0/archapi/frameworks/express_ts/adapter.py +237 -0
  11. archapi-0.3.0/archapi/frameworks/fastapi_adapter.py +184 -0
  12. archapi-0.3.0/archapi/frameworks/generic.py +211 -0
  13. archapi-0.3.0/archapi/frameworks/registry.py +28 -0
  14. archapi-0.3.0/archapi/generation/__init__.py +0 -0
  15. archapi-0.3.0/archapi/genome/__init__.py +0 -0
  16. archapi-0.3.0/archapi/indexing/__init__.py +0 -0
  17. archapi-0.3.0/archapi/indexing/cache.py +141 -0
  18. archapi-0.3.0/archapi/mapping/__init__.py +0 -0
  19. archapi-0.3.0/archapi/planning/__init__.py +0 -0
  20. archapi-0.3.0/archapi/planning/intent_planner.py +175 -0
  21. archapi-0.3.0/archapi/planning/task_dag.py +81 -0
  22. archapi-0.3.0/archapi/scanner/__init__.py +0 -0
  23. archapi-0.3.0/archapi/security/__init__.py +0 -0
  24. archapi-0.3.0/archapi/security/context_redactor.py +38 -0
  25. archapi-0.3.0/archapi/security/policy_gate.py +70 -0
  26. archapi-0.3.0/archapi/security/secret_scanner.py +90 -0
  27. archapi-0.3.0/archapi/types.py +103 -0
  28. archapi-0.3.0/archapi/validation/__init__.py +0 -0
  29. archapi-0.3.0/archapi/validation/architecture_score.py +77 -0
  30. archapi-0.3.0/archapi/validation/basic_validators.py +38 -0
  31. archapi-0.3.0/archapi/validation/command_validator.py +146 -0
  32. archapi-0.3.0/archapi.egg-info/PKG-INFO +79 -0
  33. archapi-0.3.0/archapi.egg-info/SOURCES.txt +36 -0
  34. archapi-0.3.0/archapi.egg-info/dependency_links.txt +1 -0
  35. archapi-0.3.0/archapi.egg-info/top_level.txt +1 -0
  36. archapi-0.3.0/pyproject.toml +36 -0
  37. archapi-0.3.0/setup.cfg +4 -0
  38. archapi-0.3.0/tests/test_archapi_suite.py +118 -0
archapi-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rohith Chikkala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files, to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy,
8
+ modify, merge, publish, distribute, sublicense, and/or sell copies of the
9
+ Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
archapi-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: archapi
3
+ Version: 0.3.0
4
+ Summary: Architecture-preserving REST API synthesis library
5
+ Author: ArchAPI Research Team
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rohith5005/archapi
8
+ Project-URL: Repository, https://github.com/rohith5005/archapi
9
+ Project-URL: Issues, https://github.com/rohith5005/archapi/issues
10
+ Keywords: rest-api,code-generation,architecture,express,fastapi
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Code Generators
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Dynamic: license-file
23
+
24
+ # ArchAPI
25
+
26
+ ArchAPI is a Python library for architecture-preserving REST API generation.
27
+
28
+ It scans an existing backend project, detects the framework, understands the project structure, plans a REST API, generates framework-specific files, validates the output, and writes files only when explicitly requested.
29
+
30
+ ---
31
+
32
+ ## Current Status
33
+
34
+ Current checkpoint: **Phase 3 complete**
35
+
36
+ Completed:
37
+
38
+ - Functional Python package
39
+ - Express TypeScript adapter
40
+ - FastAPI adapter
41
+ - Generic fallback adapter
42
+ - Framework detection
43
+ - Project scanning
44
+ - API architecture model
45
+ - Confidence scoring
46
+ - Low-confidence blocking
47
+ - Strict config mode
48
+ - REST intent planner
49
+ - Code generation
50
+ - Dry-run generation
51
+ - Safe apply
52
+ - Overwrite protection
53
+ - Cache and changed-file detection
54
+ - Secret scanner
55
+ - Context redaction
56
+ - Policy gate
57
+ - Architecture consistency score
58
+ - Command validation
59
+ - Unified regression test suite
60
+
61
+ ---
62
+
63
+ ## Supported Frameworks
64
+
65
+ Dedicated generation support currently exists for:
66
+
67
+ - Express TypeScript
68
+ - FastAPI
69
+
70
+ Other frameworks may be detected, but they currently use the generic fallback adapter.
71
+
72
+ ---
73
+
74
+ ## Installation
75
+
76
+ From the project root:
77
+
78
+ ```bash
79
+ pip install -e .
@@ -0,0 +1,56 @@
1
+ # ArchAPI
2
+
3
+ ArchAPI is a Python library for architecture-preserving REST API generation.
4
+
5
+ It scans an existing backend project, detects the framework, understands the project structure, plans a REST API, generates framework-specific files, validates the output, and writes files only when explicitly requested.
6
+
7
+ ---
8
+
9
+ ## Current Status
10
+
11
+ Current checkpoint: **Phase 3 complete**
12
+
13
+ Completed:
14
+
15
+ - Functional Python package
16
+ - Express TypeScript adapter
17
+ - FastAPI adapter
18
+ - Generic fallback adapter
19
+ - Framework detection
20
+ - Project scanning
21
+ - API architecture model
22
+ - Confidence scoring
23
+ - Low-confidence blocking
24
+ - Strict config mode
25
+ - REST intent planner
26
+ - Code generation
27
+ - Dry-run generation
28
+ - Safe apply
29
+ - Overwrite protection
30
+ - Cache and changed-file detection
31
+ - Secret scanner
32
+ - Context redaction
33
+ - Policy gate
34
+ - Architecture consistency score
35
+ - Command validation
36
+ - Unified regression test suite
37
+
38
+ ---
39
+
40
+ ## Supported Frameworks
41
+
42
+ Dedicated generation support currently exists for:
43
+
44
+ - Express TypeScript
45
+ - FastAPI
46
+
47
+ Other frameworks may be detected, but they currently use the generic fallback adapter.
48
+
49
+ ---
50
+
51
+ ## Installation
52
+
53
+ From the project root:
54
+
55
+ ```bash
56
+ pip install -e .
@@ -0,0 +1,3 @@
1
+ from archapi.core import ArchAPI
2
+
3
+ __all__ = ["ArchAPI"]
@@ -0,0 +1,295 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from archapi.frameworks.detector import FrameworkDetector
7
+ from archapi.frameworks.registry import FrameworkRegistry
8
+ from archapi.indexing.cache import CacheManager
9
+ from archapi.security.secret_scanner import SecretScanner
10
+ from archapi.security.context_redactor import ContextRedactor
11
+ from archapi.security.policy_gate import PolicyGate
12
+ from archapi.validation.architecture_score import ArchitectureConsistencyScorer
13
+ from archapi.validation.command_validator import CommandValidator
14
+ from archapi.types import (
15
+ APIGenome,
16
+ APIPlan,
17
+ DetectionResult,
18
+ GenerationResult,
19
+ ScanResult,
20
+ )
21
+
22
+
23
+ class ArchAPI:
24
+ def __init__(self, project_path: Union[str, Path], framework: Optional[str] = None, config: Optional[Dict[str, Any]] = None):
25
+ self.project_path = Path(project_path).resolve()
26
+ if not self.project_path.exists():
27
+ raise FileNotFoundError(f"Project path does not exist: {self.project_path}")
28
+
29
+ self._framework_override = framework
30
+ self._config = config or {}
31
+ self._detector = FrameworkDetector()
32
+ self._registry = FrameworkRegistry()
33
+ self._cache = CacheManager(self.project_path)
34
+ self._secret_scanner = SecretScanner(self.project_path)
35
+ self._context_redactor = ContextRedactor()
36
+ self._policy_gate = PolicyGate()
37
+ self._architecture_scorer = ArchitectureConsistencyScorer()
38
+ self._command_validator = CommandValidator(self.project_path)
39
+
40
+ self._detection: Optional[DetectionResult] = None
41
+ self._scan: Optional[ScanResult] = None
42
+ self._maps: Optional[Dict[str, Any]] = None
43
+ self._genome: Optional[APIGenome] = None
44
+
45
+ def detect_framework(self) -> DetectionResult:
46
+ if self._framework_override:
47
+ self._detection = DetectionResult(
48
+ framework=self._framework_override,
49
+ confidence=1.0,
50
+ reasons=["Framework explicitly provided"],
51
+ )
52
+ return self._detection
53
+
54
+ self._detection = self._detector.detect(self.project_path)
55
+ return self._detection
56
+
57
+ def _adapter(self):
58
+ detection = self._detection or self.detect_framework()
59
+ return self._registry.get(detection.framework)
60
+
61
+ def _has_config_hints(self) -> bool:
62
+ hint_keys = {
63
+ "route_dir",
64
+ "controller_dir",
65
+ "service_dir",
66
+ "model_dir",
67
+ "schema_dir",
68
+ "middleware_dir",
69
+ "test_dir",
70
+ }
71
+ return any(key in self._config for key in hint_keys)
72
+
73
+ def scan(self) -> ScanResult:
74
+ detection = self._detection or self.detect_framework()
75
+
76
+ # Strict config mode:
77
+ # If user provides architecture hints, scan ONLY those hinted directories.
78
+ # This prevents accidental scanning of the library repo, sample projects,
79
+ # caches, or unrelated test files.
80
+ if self._has_config_hints():
81
+ self._scan = ScanResult(
82
+ framework=detection.framework,
83
+ project_path=self.project_path,
84
+ )
85
+ self._apply_config_hints_to_scan(self._scan)
86
+ return self._scan
87
+
88
+ adapter = self._adapter()
89
+ self._scan = adapter.scan(self.project_path)
90
+ self._scan.framework = detection.framework
91
+ return self._scan
92
+
93
+ def _apply_config_hints_to_scan(self, scan: ScanResult) -> None:
94
+ """
95
+ Applies user-provided architecture hints.
96
+
97
+ Supported config keys:
98
+ - route_dir
99
+ - controller_dir
100
+ - service_dir
101
+ - model_dir
102
+ - schema_dir
103
+ - middleware_dir
104
+ - test_dir
105
+ """
106
+
107
+ hint_map = {
108
+ "route_dir": scan.routes,
109
+ "controller_dir": scan.controllers,
110
+ "service_dir": scan.services,
111
+ "model_dir": scan.models,
112
+ "schema_dir": scan.schemas,
113
+ "middleware_dir": scan.middleware,
114
+ "test_dir": scan.tests,
115
+ }
116
+
117
+ ignored_parts = {
118
+ ".git",
119
+ ".venv",
120
+ "node_modules",
121
+ "dist",
122
+ "build",
123
+ "coverage",
124
+ "__pycache__",
125
+ ".archapi",
126
+ "archapi.egg-info",
127
+ }
128
+
129
+ for config_key, target_list in hint_map.items():
130
+ raw_dir = self._config.get(config_key)
131
+ if not raw_dir:
132
+ continue
133
+
134
+ hint_path = (self.project_path / raw_dir).resolve()
135
+
136
+ if not hint_path.exists() or not hint_path.is_dir():
137
+ continue
138
+
139
+ for file_path in hint_path.rglob("*"):
140
+ if not file_path.is_file():
141
+ continue
142
+
143
+ try:
144
+ rel_parts = file_path.relative_to(self.project_path).parts
145
+ except ValueError:
146
+ rel_parts = file_path.parts
147
+
148
+ if any(part in ignored_parts for part in rel_parts):
149
+ continue
150
+
151
+ if file_path not in target_list:
152
+ target_list.append(file_path)
153
+
154
+ def config(self) -> Dict[str, Any]:
155
+ return dict(self._config)
156
+
157
+ def build_maps(self) -> Dict[str, Any]:
158
+ scan = self._scan or self.scan()
159
+ adapter = self._adapter()
160
+ self._maps = adapter.build_maps(scan)
161
+ return self._maps
162
+
163
+ def extract_genome(self) -> APIGenome:
164
+ scan = self._scan or self.scan()
165
+ maps = self._maps or self.build_maps()
166
+ adapter = self._adapter()
167
+ self._genome = adapter.extract_genome(maps, scan)
168
+ self._genome.framework = (self._detection or self.detect_framework()).framework
169
+ return self._genome
170
+
171
+ def compute_confidence(self) -> Dict[str, Any]:
172
+ detection = self._detection or self.detect_framework()
173
+ genome = self._genome or self.extract_genome()
174
+
175
+ missing = []
176
+
177
+ if genome.route_style == "unknown":
178
+ missing.append("route style")
179
+ if genome.controller_style == "unknown":
180
+ missing.append("controller style")
181
+ if genome.service_style == "unknown":
182
+ missing.append("service style")
183
+ if genome.schema_style == "unknown":
184
+ missing.append("schema style")
185
+
186
+ # Overall confidence should consider BOTH:
187
+ # 1. framework detection confidence
188
+ # 2. API architecture/genome confidence
189
+ #
190
+ # This prevents generic/unknown projects from being treated as safe
191
+ # just because some folders accidentally look like routes/services.
192
+ overall = round(min(detection.confidence, genome.confidence), 2)
193
+
194
+ mode = "generate"
195
+
196
+ if detection.framework in {"generic", "node-unknown"}:
197
+ mode = "blocked"
198
+ elif overall < 0.30:
199
+ mode = "blocked"
200
+ elif overall < 0.55:
201
+ mode = "plan_only"
202
+ elif overall < 0.75:
203
+ mode = "generate_with_warnings"
204
+
205
+ return {
206
+ "overall": overall,
207
+ "detection_confidence": detection.confidence,
208
+ "genome_confidence": genome.confidence,
209
+ "mode": mode,
210
+ "missing": missing,
211
+ "framework": genome.framework,
212
+ }
213
+
214
+ def plan_api(self, request: str) -> APIPlan:
215
+ maps = self._maps or self.build_maps()
216
+ genome = self._genome or self.extract_genome()
217
+ adapter = self._adapter()
218
+
219
+ plan = adapter.plan_api(request, genome, maps)
220
+ confidence = self.compute_confidence()
221
+
222
+ if confidence["mode"] in {"blocked", "plan_only"}:
223
+ plan.generation_allowed = False
224
+
225
+ if confidence["framework"] in {"generic", "node-unknown"}:
226
+ plan.reason = (
227
+ "Framework could not be confidently detected. "
228
+ "Provide framework or config before generation."
229
+ )
230
+ else:
231
+ plan.reason = (
232
+ "Architecture confidence too low for code generation. "
233
+ f"Missing: {', '.join(confidence['missing']) or 'unknown'}"
234
+ )
235
+
236
+ return plan
237
+
238
+ def save_cache(self) -> Dict[str, Path]:
239
+ detection = self._detection or self.detect_framework()
240
+ scan = self._scan or self.scan()
241
+ maps = self._maps or self.build_maps()
242
+ genome = self._genome or self.extract_genome()
243
+
244
+ return self._cache.save_snapshot(
245
+ detection=detection,
246
+ scan=scan,
247
+ maps=maps,
248
+ genome=genome,
249
+ )
250
+
251
+ def changed_files(self) -> list:
252
+ return self._cache.changed_files()
253
+
254
+ def scan_secrets(self):
255
+ return self._secret_scanner.scan()
256
+
257
+ def redact_context(self, text: str) -> str:
258
+ return self._context_redactor.redact(text)
259
+
260
+ def validate_policy(self, result: GenerationResult):
261
+ return self._policy_gate.validate_result(result)
262
+
263
+ def score_architecture(self, result: GenerationResult):
264
+ genome = self._genome or self.extract_genome()
265
+ return self._architecture_scorer.score(result.files, genome)
266
+
267
+ def validate_project_commands(self):
268
+ detection = self._detection or self.detect_framework()
269
+
270
+ if detection.framework in {"express-typescript", "nestjs", "node-unknown"}:
271
+ return self._command_validator.validate_node_project()
272
+
273
+ return self._command_validator.validate_node_project()
274
+
275
+ def generate_api(self, request: str, dry_run: bool = True) -> GenerationResult:
276
+ maps = self._maps or self.build_maps()
277
+ genome = self._genome or self.extract_genome()
278
+ plan = self.plan_api(request)
279
+
280
+ adapter = self._adapter()
281
+ files = adapter.generate_code(plan, genome, maps)
282
+ report = adapter.validate_generated_code(files, plan, genome)
283
+
284
+ result = GenerationResult(
285
+ project_path=self.project_path,
286
+ plan=plan,
287
+ files=files,
288
+ validation_report=report,
289
+ warnings=report.warnings,
290
+ )
291
+
292
+ if not dry_run and report.success:
293
+ result.apply()
294
+
295
+ return result
File without changes
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+
7
+ from archapi.types import (
8
+ APIPlan,
9
+ APIGenome,
10
+ DetectionResult,
11
+ GeneratedFile,
12
+ ScanResult,
13
+ ValidationReport,
14
+ )
15
+
16
+
17
+ class FrameworkAdapter(ABC):
18
+ name: str = "unknown"
19
+
20
+ @abstractmethod
21
+ def detect(self, project_path: Path) -> DetectionResult:
22
+ raise NotImplementedError
23
+
24
+ @abstractmethod
25
+ def scan(self, project_path: Path) -> ScanResult:
26
+ raise NotImplementedError
27
+
28
+ @abstractmethod
29
+ def build_maps(self, scan_result: ScanResult) -> Dict[str, Any]:
30
+ raise NotImplementedError
31
+
32
+ @abstractmethod
33
+ def extract_genome(self, maps: Dict[str, Any], scan_result: ScanResult) -> APIGenome:
34
+ raise NotImplementedError
35
+
36
+ @abstractmethod
37
+ def plan_api(self, request: str, genome: APIGenome, maps: Dict[str, Any]) -> APIPlan:
38
+ raise NotImplementedError
39
+
40
+ @abstractmethod
41
+ def generate_code(
42
+ self,
43
+ plan: APIPlan,
44
+ genome: APIGenome,
45
+ maps: Dict[str, Any],
46
+ ) -> List[GeneratedFile]:
47
+ raise NotImplementedError
48
+
49
+ @abstractmethod
50
+ def validate_generated_code(
51
+ self,
52
+ files: List[GeneratedFile],
53
+ plan: APIPlan,
54
+ genome: APIGenome,
55
+ ) -> ValidationReport:
56
+ raise NotImplementedError
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from archapi.types import DetectionResult
8
+
9
+
10
+ class FrameworkDetector:
11
+ def detect(self, project_path: Path) -> DetectionResult:
12
+ candidates: List[DetectionResult] = []
13
+
14
+ package_json = project_path / "package.json"
15
+ if package_json.exists():
16
+ try:
17
+ data = json.loads(package_json.read_text(encoding="utf-8"))
18
+ deps = {}
19
+ deps.update(data.get("dependencies", {}))
20
+ deps.update(data.get("devDependencies", {}))
21
+
22
+ if "@nestjs/core" in deps:
23
+ candidates.append(
24
+ DetectionResult("nestjs", 0.95, ["package.json contains @nestjs/core"])
25
+ )
26
+
27
+ if "express" in deps:
28
+ candidates.append(
29
+ DetectionResult("express-typescript", 0.85, ["package.json contains express"])
30
+ )
31
+ except Exception:
32
+ candidates.append(
33
+ DetectionResult("node-unknown", 0.30, ["package.json exists but could not be parsed"])
34
+ )
35
+
36
+ if (project_path / "manage.py").exists():
37
+ candidates.append(
38
+ DetectionResult("django-drf", 0.75, ["manage.py detected"])
39
+ )
40
+
41
+ py_text = ""
42
+ pyproject = project_path / "pyproject.toml"
43
+ requirements = project_path / "requirements.txt"
44
+
45
+ if pyproject.exists():
46
+ py_text += pyproject.read_text(encoding="utf-8", errors="ignore").lower()
47
+
48
+ if requirements.exists():
49
+ py_text += requirements.read_text(encoding="utf-8", errors="ignore").lower()
50
+
51
+ if "fastapi" in py_text:
52
+ candidates.append(
53
+ DetectionResult("fastapi", 0.90, ["fastapi dependency detected"])
54
+ )
55
+
56
+ if "flask" in py_text:
57
+ candidates.append(
58
+ DetectionResult("flask", 0.85, ["flask dependency detected"])
59
+ )
60
+
61
+ if (project_path / "pom.xml").exists() or (project_path / "build.gradle").exists():
62
+ candidates.append(
63
+ DetectionResult("spring-boot", 0.70, ["Java build file detected"])
64
+ )
65
+
66
+ if list(project_path.glob("*.csproj")):
67
+ candidates.append(
68
+ DetectionResult("dotnet-core", 0.75, [".csproj file detected"])
69
+ )
70
+
71
+ if (project_path / "composer.json").exists():
72
+ composer = (project_path / "composer.json").read_text(
73
+ encoding="utf-8",
74
+ errors="ignore",
75
+ ).lower()
76
+ if "laravel" in composer:
77
+ candidates.append(
78
+ DetectionResult("laravel", 0.90, ["composer.json contains laravel"])
79
+ )
80
+
81
+ if (project_path / "Gemfile").exists():
82
+ gemfile = (project_path / "Gemfile").read_text(
83
+ encoding="utf-8",
84
+ errors="ignore",
85
+ ).lower()
86
+ if "rails" in gemfile:
87
+ candidates.append(
88
+ DetectionResult("rails", 0.90, ["Gemfile contains rails"])
89
+ )
90
+
91
+ if (project_path / "go.mod").exists():
92
+ go_mod = (project_path / "go.mod").read_text(
93
+ encoding="utf-8",
94
+ errors="ignore",
95
+ ).lower()
96
+
97
+ if "gin-gonic/gin" in go_mod or "gofiber/fiber" in go_mod:
98
+ candidates.append(
99
+ DetectionResult("go-api", 0.85, ["go.mod contains Gin or Fiber"])
100
+ )
101
+ else:
102
+ candidates.append(
103
+ DetectionResult("go-api", 0.55, ["go.mod detected"])
104
+ )
105
+
106
+ if not candidates:
107
+ return DetectionResult(
108
+ framework="generic",
109
+ confidence=0.10,
110
+ reasons=["No known framework markers detected"],
111
+ )
112
+
113
+ return sorted(candidates, key=lambda item: item.confidence, reverse=True)[0]