sourcecode 0.1.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.
sourcecode/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,177 @@
1
+ """Clasificacion de tipo de proyecto y scoring de confianza."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterable, Sequence
5
+ from dataclasses import replace
6
+ from typing import Any, Literal
7
+
8
+ from sourcecode.schema import EntryPoint, StackDetection
9
+ from sourcecode.tree_utils import flatten_file_tree
10
+
11
+ _API_FRAMEWORKS = {
12
+ "FastAPI",
13
+ "Django",
14
+ "Flask",
15
+ "Express",
16
+ "Gin",
17
+ "Echo",
18
+ "Axum",
19
+ "Actix Web",
20
+ "Rocket",
21
+ "Spring Boot",
22
+ "Quarkus",
23
+ "Laravel",
24
+ "Symfony",
25
+ }
26
+ _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter"}
27
+ _CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
28
+ ConfidenceLevel = Literal["high", "medium", "low"]
29
+
30
+
31
+ class TypeClassifier:
32
+ """Clasifica project_type y enriquece stacks con confianza/primary."""
33
+
34
+ def enrich(
35
+ self,
36
+ file_tree: dict[str, Any],
37
+ stacks: Sequence[StackDetection],
38
+ entry_points: Sequence[EntryPoint],
39
+ ) -> tuple[list[StackDetection], str | None]:
40
+ enriched = [self._enrich_stack(file_tree, stack, entry_points) for stack in stacks]
41
+ project_type = self._classify_project_type(file_tree, enriched, entry_points)
42
+ primary_stack = self._select_primary_stack(enriched, project_type)
43
+
44
+ final_stacks: list[StackDetection] = []
45
+ for stack in enriched:
46
+ final_stacks.append(replace(stack, primary=(stack.stack == primary_stack)))
47
+ return final_stacks, project_type
48
+
49
+ def _enrich_stack(
50
+ self,
51
+ file_tree: dict[str, Any],
52
+ stack: StackDetection,
53
+ entry_points: Sequence[EntryPoint],
54
+ ) -> StackDetection:
55
+ signals = list(stack.signals)
56
+ score = 0
57
+
58
+ if stack.manifests:
59
+ score += 4
60
+ signals.extend(f"manifest:{manifest}" for manifest in stack.manifests)
61
+ if stack.frameworks:
62
+ score += 2
63
+ signals.extend(f"framework:{framework.name}" for framework in stack.frameworks)
64
+ if stack.package_manager:
65
+ score += 2
66
+ signals.append(f"package_manager:{stack.package_manager}")
67
+
68
+ matching_entry_points = [entry for entry in entry_points if entry.stack == stack.stack]
69
+ if matching_entry_points:
70
+ score += 2
71
+ signals.extend(f"entry:{entry.path}" for entry in matching_entry_points)
72
+
73
+ extension_hits = self._count_extension_hits(file_tree, stack.stack)
74
+ if extension_hits >= 2:
75
+ score += 1
76
+ signals.append(f"extensions:{extension_hits}")
77
+
78
+ if stack.detection_method == "heuristic":
79
+ score -= 2
80
+ signals.append("method:heuristic")
81
+
82
+ confidence = self._score_to_confidence(score)
83
+ return replace(stack, confidence=confidence, signals=self._unique(signals))
84
+
85
+ def _classify_project_type(
86
+ self,
87
+ file_tree: dict[str, Any],
88
+ stacks: Sequence[StackDetection],
89
+ entry_points: Sequence[EntryPoint],
90
+ ) -> str | None:
91
+ flat_paths = set(flatten_file_tree(file_tree))
92
+ stack_names = {stack.stack for stack in stacks}
93
+ framework_names = {framework.name for stack in stacks for framework in stack.frameworks}
94
+
95
+ if len(stack_names) >= 2 and self._is_fullstack(stacks):
96
+ return "fullstack"
97
+
98
+ if "src/lib.rs" in flat_paths and not any(path.endswith("main.rs") for path in flat_paths):
99
+ return "library"
100
+
101
+ if framework_names & _WEB_FRAMEWORKS or any(
102
+ path.startswith(("app/", "pages/", "components/")) for path in flat_paths
103
+ ):
104
+ return "webapp"
105
+
106
+ if framework_names & _API_FRAMEWORKS:
107
+ return "api"
108
+
109
+ if framework_names & _CLI_FRAMEWORKS or any(
110
+ entry.kind == "cli" for entry in entry_points
111
+ ) or any(path.startswith("bin/") for path in flat_paths):
112
+ return "cli"
113
+
114
+ if stack_names:
115
+ single = next(iter(stack_names))
116
+ if single in {"python", "go", "java", "php", "ruby"} and framework_names & _API_FRAMEWORKS:
117
+ return "api"
118
+
119
+ return "unknown" if stacks else None
120
+
121
+ def _is_fullstack(self, stacks: Sequence[StackDetection]) -> bool:
122
+ has_web = False
123
+ has_api = False
124
+ for stack in stacks:
125
+ frameworks = {framework.name for framework in stack.frameworks}
126
+ if frameworks & _WEB_FRAMEWORKS or stack.stack == "nodejs":
127
+ has_web = True
128
+ if frameworks & _API_FRAMEWORKS or stack.stack in {"python", "go", "java", "php", "ruby"}:
129
+ has_api = True
130
+ return has_web and has_api
131
+
132
+ def _select_primary_stack(
133
+ self, stacks: Sequence[StackDetection], project_type: str | None
134
+ ) -> str | None:
135
+ if not stacks:
136
+ return None
137
+
138
+ def sort_key(stack: StackDetection) -> tuple[int, int, int]:
139
+ score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
140
+ manifest_weight = 1 if stack.manifests else 0
141
+ priority = 0
142
+ if project_type in {"webapp", "fullstack"} and stack.stack == "nodejs" or project_type == "api" and stack.stack in {"python", "go", "java", "php", "ruby"} or project_type == "cli" and any(
143
+ framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
144
+ ):
145
+ priority = 3
146
+ return (priority, score, manifest_weight)
147
+
148
+ return max(stacks, key=sort_key).stack
149
+
150
+ def _count_extension_hits(self, file_tree: dict[str, Any], stack: str) -> int:
151
+ extensions = {
152
+ "python": (".py",),
153
+ "nodejs": (".js", ".ts", ".tsx"),
154
+ "go": (".go",),
155
+ "rust": (".rs",),
156
+ "java": (".java",),
157
+ "php": (".php",),
158
+ "ruby": (".rb",),
159
+ "dart": (".dart",),
160
+ }.get(stack, ())
161
+ return sum(1 for path in flatten_file_tree(file_tree) if path.endswith(extensions))
162
+
163
+ def _score_to_confidence(self, score: int) -> ConfidenceLevel:
164
+ if score >= 6:
165
+ return "high"
166
+ if score >= 3:
167
+ return "medium"
168
+ return "low"
169
+
170
+ def _unique(self, values: Iterable[str]) -> list[str]:
171
+ seen: set[str] = set()
172
+ ordered: list[str] = []
173
+ for value in values:
174
+ if value not in seen:
175
+ seen.add(value)
176
+ ordered.append(value)
177
+ return ordered
sourcecode/cli.py ADDED
@@ -0,0 +1,208 @@
1
+ """CLI de sourcecode — interfaz de linea de comandos principal."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ import typer
9
+
10
+ from sourcecode import __version__
11
+
12
+ app = typer.Typer(
13
+ name="sourcecode",
14
+ help="Genera un mapa de contexto estructurado del proyecto para agentes IA.",
15
+ add_completion=False,
16
+ )
17
+
18
+ FORMAT_CHOICES = ["json", "yaml"]
19
+
20
+
21
+ def version_callback(value: bool) -> None:
22
+ if value:
23
+ typer.echo(f"sourcecode {__version__}")
24
+ raise typer.Exit()
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def main(
29
+ ctx: typer.Context,
30
+ path: Path = typer.Argument(Path("."), help="Directorio a analizar (default: directorio actual)"),
31
+ format: str = typer.Option(
32
+ "json",
33
+ "--format",
34
+ "-f",
35
+ help="Formato de salida: json|yaml",
36
+ show_default=True,
37
+ ),
38
+ output: Optional[Path] = typer.Option(
39
+ None,
40
+ "--output",
41
+ "-o",
42
+ help="Fichero de salida (default: stdout)",
43
+ ),
44
+ compact: bool = typer.Option(
45
+ False,
46
+ "--compact",
47
+ help="Output reducido (~500 tokens): tipo de proyecto, stacks, entradas y arbol nivel 1",
48
+ ),
49
+ no_redact: bool = typer.Option(
50
+ False,
51
+ "--no-redact",
52
+ help="Desactivar redaccion de secretos (activa por defecto)",
53
+ ),
54
+ version: Optional[bool] = typer.Option(
55
+ None,
56
+ "--version",
57
+ "-v",
58
+ callback=version_callback,
59
+ is_eager=True,
60
+ help="Mostrar version y salir",
61
+ ),
62
+ depth: int = typer.Option(
63
+ 4,
64
+ "--depth",
65
+ help="Profundidad maxima del arbol de ficheros (default: 4)",
66
+ min=1,
67
+ max=20,
68
+ ),
69
+ ) -> None:
70
+ """Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
71
+ # Validar formato
72
+ if format not in FORMAT_CHOICES:
73
+ typer.echo(
74
+ f"Error: valor invalido '{format}' para --format. Opciones: {', '.join(FORMAT_CHOICES)}",
75
+ err=True,
76
+ )
77
+ raise typer.Exit(code=1)
78
+
79
+ # Resolver y validar path
80
+ target = path.resolve()
81
+ if not target.exists():
82
+ typer.echo(f"Error: el directorio '{target}' no existe.", err=True)
83
+ raise typer.Exit(code=1)
84
+ if not target.is_dir():
85
+ typer.echo(f"Error: '{target}' no es un directorio.", err=True)
86
+ raise typer.Exit(code=1)
87
+
88
+ # --- Importar modulos de logica ---
89
+ from dataclasses import asdict, replace
90
+
91
+ from sourcecode.detectors import ProjectDetector, build_default_detectors
92
+ from sourcecode.redactor import SecretRedactor, redact_dict
93
+ from sourcecode.scanner import FileScanner
94
+ from sourcecode.schema import AnalysisMetadata, EntryPoint, SourceMap, StackDetection
95
+ from sourcecode.serializer import compact_view, write_output
96
+ from sourcecode.workspace import WorkspaceAnalyzer
97
+
98
+ # 1. Escanear el directorio (SCAN-01 a SCAN-05)
99
+ redactor = SecretRedactor(enabled=not no_redact)
100
+ scanner = FileScanner(target, max_depth=depth)
101
+ raw_tree = scanner.scan_tree()
102
+
103
+ # 2. Filtrar del arbol las entradas de .env y *.secret (SEC-02, todos los niveles)
104
+ def filter_sensitive_files(tree: dict[str, Any]) -> dict[str, Any]:
105
+ filtered: dict[str, Any] = {}
106
+ for name, value in tree.items():
107
+ if redactor.should_exclude_file(name):
108
+ continue # excluir .env, *.secret del arbol
109
+ if isinstance(value, dict):
110
+ filtered[name] = filter_sensitive_files(value)
111
+ else:
112
+ filtered[name] = value
113
+ return filtered
114
+
115
+ file_tree = filter_sensitive_files(raw_tree)
116
+ manifests = scanner.find_manifests()
117
+ detector = ProjectDetector(build_default_detectors())
118
+ workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
119
+
120
+ root_manifests = [
121
+ manifest
122
+ for manifest in manifests
123
+ if Path(manifest).resolve().parent == target
124
+ ]
125
+ detection_manifests = root_manifests if workspace_analysis.workspaces else manifests
126
+ if workspace_analysis.is_monorepo and not root_manifests:
127
+ stacks: list[StackDetection] = []
128
+ entry_points: list[EntryPoint] = []
129
+ else:
130
+ stacks, entry_points, _project_type = detector.detect(target, file_tree, detection_manifests)
131
+
132
+ for workspace in workspace_analysis.workspaces:
133
+ workspace_root = target / workspace.path
134
+ if not workspace_root.exists() or not workspace_root.is_dir():
135
+ continue
136
+ workspace_scanner = FileScanner(workspace_root, max_depth=depth)
137
+ workspace_tree = filter_sensitive_files(workspace_scanner.scan_tree())
138
+ workspace_manifests = workspace_scanner.find_manifests()
139
+ workspace_stacks, workspace_entry_points, _ = detector.detect(
140
+ workspace_root,
141
+ workspace_tree,
142
+ workspace_manifests,
143
+ )
144
+
145
+ stacks.extend(
146
+ replace(stack, root=workspace.path, workspace=workspace.path, primary=False)
147
+ for stack in workspace_stacks
148
+ )
149
+ entry_points.extend(
150
+ replace(
151
+ entry_point,
152
+ path=f"{workspace.path}/{entry_point.path}",
153
+ )
154
+ for entry_point in workspace_entry_points
155
+ )
156
+
157
+ stacks, project_type = detector.classify_results(
158
+ file_tree,
159
+ stacks,
160
+ entry_points,
161
+ project_type_override="monorepo" if workspace_analysis.is_monorepo else None,
162
+ )
163
+
164
+ # 3. Construir el schema
165
+ metadata = AnalysisMetadata(analyzed_path=str(target))
166
+ sm = SourceMap(
167
+ metadata=metadata,
168
+ file_tree=file_tree,
169
+ stacks=stacks,
170
+ project_type=project_type,
171
+ entry_points=entry_points,
172
+ )
173
+
174
+ # 4. Serializar (con o sin modo compact)
175
+ if compact:
176
+ data = compact_view(sm)
177
+ # Aplicar redaccion sobre el dict del compact view
178
+ if not no_redact:
179
+ data = redact_dict(data)
180
+ content = json.dumps(data, indent=2, ensure_ascii=False)
181
+ else:
182
+ # Redactar sobre el dict serializado (SEC-01, SEC-03)
183
+ raw_dict = asdict(sm)
184
+ if not no_redact:
185
+ raw_dict = redact_dict(raw_dict)
186
+
187
+ if format == "yaml":
188
+ # Para YAML, serializar el dict directamente con ruamel.yaml
189
+ from io import StringIO
190
+
191
+ from ruamel.yaml import YAML
192
+
193
+ yaml = YAML()
194
+ yaml.default_flow_style = False
195
+ yaml.representer.add_representer(
196
+ type(None),
197
+ lambda dumper, data_val: dumper.represent_scalar(
198
+ "tag:yaml.org,2002:null", "null"
199
+ ),
200
+ )
201
+ stream = StringIO()
202
+ yaml.dump(raw_dict, stream)
203
+ content = stream.getvalue()
204
+ else:
205
+ content = json.dumps(raw_dict, indent=2, ensure_ascii=False)
206
+
207
+ # 5. Escribir output (CLI-04)
208
+ write_output(content, output=output)
@@ -0,0 +1,54 @@
1
+ """Infraestructura de deteccion para sourcecode."""
2
+
3
+ from sourcecode.detectors.base import (
4
+ AbstractDetector,
5
+ DetectionContext,
6
+ EntryPoint,
7
+ FrameworkDetection,
8
+ StackDetection,
9
+ )
10
+ from sourcecode.detectors.dart import DartDetector
11
+ from sourcecode.detectors.go import GoDetector
12
+ from sourcecode.detectors.heuristic import HeuristicDetector
13
+ from sourcecode.detectors.java import JavaDetector
14
+ from sourcecode.detectors.nodejs import NodejsDetector
15
+ from sourcecode.detectors.php import PhpDetector
16
+ from sourcecode.detectors.project import ProjectDetector
17
+ from sourcecode.detectors.python import PythonDetector
18
+ from sourcecode.detectors.ruby import RubyDetector
19
+ from sourcecode.detectors.rust import RustDetector
20
+
21
+
22
+ def build_default_detectors() -> list[AbstractDetector]:
23
+ """Registro por defecto de detectores para la CLI."""
24
+ return [
25
+ NodejsDetector(),
26
+ PythonDetector(),
27
+ GoDetector(),
28
+ RustDetector(),
29
+ JavaDetector(),
30
+ PhpDetector(),
31
+ RubyDetector(),
32
+ DartDetector(),
33
+ HeuristicDetector(),
34
+ ]
35
+
36
+
37
+ __all__ = [
38
+ "AbstractDetector",
39
+ "build_default_detectors",
40
+ "DartDetector",
41
+ "DetectionContext",
42
+ "EntryPoint",
43
+ "FrameworkDetection",
44
+ "GoDetector",
45
+ "HeuristicDetector",
46
+ "JavaDetector",
47
+ "NodejsDetector",
48
+ "PhpDetector",
49
+ "PythonDetector",
50
+ "ProjectDetector",
51
+ "RubyDetector",
52
+ "RustDetector",
53
+ "StackDetection",
54
+ ]
@@ -0,0 +1,42 @@
1
+ """Contratos base de deteccion para sourcecode."""
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from sourcecode.schema import EntryPoint, FrameworkDetection, StackDetection
10
+
11
+
12
+ @dataclass
13
+ class DetectionContext:
14
+ """Contexto comun que reciben todos los detectores."""
15
+
16
+ root: Path
17
+ file_tree: dict[str, Any]
18
+ manifests: list[str] = field(default_factory=list)
19
+
20
+
21
+ class AbstractDetector(ABC):
22
+ """Contrato base para detectores de stack."""
23
+
24
+ name: str = "abstract"
25
+ priority: int = 100
26
+
27
+ @abstractmethod
28
+ def can_detect(self, context: DetectionContext) -> bool:
29
+ """Indica si este detector aplica al proyecto dado."""
30
+
31
+ @abstractmethod
32
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
33
+ """Produce stacks y entry points detectados."""
34
+
35
+
36
+ __all__ = [
37
+ "AbstractDetector",
38
+ "DetectionContext",
39
+ "EntryPoint",
40
+ "FrameworkDetection",
41
+ "StackDetection",
42
+ ]
@@ -0,0 +1,42 @@
1
+ """Detector de proyectos Dart y Flutter."""
2
+ from __future__ import annotations
3
+
4
+ from sourcecode.detectors.base import (
5
+ AbstractDetector,
6
+ DetectionContext,
7
+ EntryPoint,
8
+ StackDetection,
9
+ )
10
+ from sourcecode.detectors.parsers import read_text_lines
11
+ from sourcecode.schema import FrameworkDetection
12
+ from sourcecode.tree_utils import path_exists_in_tree
13
+
14
+
15
+ class DartDetector(AbstractDetector):
16
+ name = "dart"
17
+ priority = 90
18
+
19
+ def can_detect(self, context: DetectionContext) -> bool:
20
+ return "pubspec.yaml" in context.manifests
21
+
22
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
23
+ content = "\n".join(read_text_lines(context.root / "pubspec.yaml")).lower()
24
+ frameworks: list[FrameworkDetection] = []
25
+ if "flutter:" in content:
26
+ frameworks.append(FrameworkDetection(name="Flutter", source="pubspec.yaml"))
27
+
28
+ entry_points: list[EntryPoint] = []
29
+ for path in ("lib/main.dart", "bin/main.dart"):
30
+ if path_exists_in_tree(context.file_tree, path):
31
+ entry_points.append(
32
+ EntryPoint(path=path, stack="dart", kind="application", source="pubspec.yaml")
33
+ )
34
+
35
+ stack = StackDetection(
36
+ stack="dart",
37
+ detection_method="manifest",
38
+ confidence="high",
39
+ frameworks=frameworks,
40
+ manifests=["pubspec.yaml"],
41
+ )
42
+ return [stack], entry_points
@@ -0,0 +1,52 @@
1
+ """Detector de proyectos Go."""
2
+ from __future__ import annotations
3
+
4
+ from sourcecode.detectors.base import (
5
+ AbstractDetector,
6
+ DetectionContext,
7
+ EntryPoint,
8
+ StackDetection,
9
+ )
10
+ from sourcecode.detectors.parsers import read_text_lines, unique_strings
11
+ from sourcecode.schema import FrameworkDetection
12
+ from sourcecode.tree_utils import flatten_file_tree
13
+
14
+ _FRAMEWORK_MAP = {
15
+ "github.com/gin-gonic/gin": "Gin",
16
+ "github.com/labstack/echo": "Echo",
17
+ "github.com/spf13/cobra": "Cobra",
18
+ "github.com/gofiber/fiber": "Fiber",
19
+ }
20
+
21
+
22
+ class GoDetector(AbstractDetector):
23
+ name = "go"
24
+ priority = 40
25
+
26
+ def can_detect(self, context: DetectionContext) -> bool:
27
+ return "go.mod" in context.manifests
28
+
29
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
30
+ lines = read_text_lines(context.root / "go.mod")
31
+ content = "\n".join(lines)
32
+ frameworks = [
33
+ FrameworkDetection(name=label, source="go.mod")
34
+ for dependency, label in _FRAMEWORK_MAP.items()
35
+ if dependency in content
36
+ ]
37
+ entry_candidates = [
38
+ path for path in flatten_file_tree(context.file_tree) if path.endswith("main.go")
39
+ ]
40
+ preferred = [path for path in entry_candidates if path.startswith("cmd/")] or entry_candidates
41
+ entry_points = [
42
+ EntryPoint(path=path, stack="go", kind="binary", source="go.mod")
43
+ for path in unique_strings(preferred)
44
+ ]
45
+ stack = StackDetection(
46
+ stack="go",
47
+ detection_method="manifest",
48
+ confidence="high",
49
+ frameworks=frameworks,
50
+ manifests=["go.mod"],
51
+ )
52
+ return [stack], entry_points
@@ -0,0 +1,70 @@
1
+ """Fallback heuristico por extension de fichero."""
2
+ from __future__ import annotations
3
+
4
+ from collections import Counter
5
+
6
+ from sourcecode.detectors.base import (
7
+ AbstractDetector,
8
+ DetectionContext,
9
+ EntryPoint,
10
+ StackDetection,
11
+ )
12
+ from sourcecode.tree_utils import flatten_file_tree
13
+
14
+ _EXTENSION_MAP = {
15
+ ".py": "python",
16
+ ".js": "nodejs",
17
+ ".ts": "nodejs",
18
+ ".tsx": "nodejs",
19
+ ".go": "go",
20
+ ".rs": "rust",
21
+ ".java": "java",
22
+ ".php": "php",
23
+ ".rb": "ruby",
24
+ ".dart": "dart",
25
+ }
26
+
27
+ _ENTRYPOINT_NAMES = {
28
+ "main.py": ("python", "script"),
29
+ "app.py": ("python", "app"),
30
+ "index.js": ("nodejs", "server"),
31
+ "main.go": ("go", "binary"),
32
+ "main.rs": ("rust", "binary"),
33
+ }
34
+
35
+
36
+ class HeuristicDetector(AbstractDetector):
37
+ name = "heuristic"
38
+ priority = 999
39
+
40
+ def can_detect(self, context: DetectionContext) -> bool:
41
+ return True
42
+
43
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
44
+ paths = flatten_file_tree(context.file_tree)
45
+ counts: Counter[str] = Counter()
46
+ for path in paths:
47
+ for extension, stack in _EXTENSION_MAP.items():
48
+ if path.endswith(extension):
49
+ counts[stack] += 1
50
+ break
51
+
52
+ stacks = [
53
+ StackDetection(
54
+ stack=stack,
55
+ detection_method="heuristic",
56
+ confidence="low",
57
+ manifests=[],
58
+ )
59
+ for stack, _count in counts.most_common()
60
+ ]
61
+
62
+ entry_points: list[EntryPoint] = []
63
+ for path in paths:
64
+ filename = path.rsplit("/", 1)[-1]
65
+ if filename in _ENTRYPOINT_NAMES:
66
+ stack, kind = _ENTRYPOINT_NAMES[filename]
67
+ entry_points.append(
68
+ EntryPoint(path=path, stack=stack, kind=kind, source="heuristic")
69
+ )
70
+ return stacks, entry_points