sourcecode 0.1.0__py3-none-any.whl → 0.2.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
sourcecode/classifier.py CHANGED
@@ -9,6 +9,7 @@ from sourcecode.schema import EntryPoint, StackDetection
9
9
  from sourcecode.tree_utils import flatten_file_tree
10
10
 
11
11
  _API_FRAMEWORKS = {
12
+ "ASP.NET Core",
12
13
  "FastAPI",
13
14
  "Django",
14
15
  "Flask",
@@ -22,9 +23,12 @@ _API_FRAMEWORKS = {
22
23
  "Quarkus",
23
24
  "Laravel",
24
25
  "Symfony",
26
+ "Ktor",
27
+ "Play",
25
28
  }
26
- _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter"}
29
+ _WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter", "Phoenix"}
27
30
  _CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
31
+ _API_STACKS = {"python", "go", "java", "php", "ruby", "dotnet", "kotlin", "scala"}
28
32
  ConfidenceLevel = Literal["high", "medium", "low"]
29
33
 
30
34
 
@@ -113,8 +117,10 @@ class TypeClassifier:
113
117
 
114
118
  if stack_names:
115
119
  single = next(iter(stack_names))
116
- if single in {"python", "go", "java", "php", "ruby"} and framework_names & _API_FRAMEWORKS:
120
+ if single in _API_STACKS and framework_names & _API_FRAMEWORKS:
117
121
  return "api"
122
+ if single in {"cpp", "dotnet"} and any(entry.kind == "cli" for entry in entry_points):
123
+ return "cli"
118
124
 
119
125
  return "unknown" if stacks else None
120
126
 
@@ -125,7 +131,7 @@ class TypeClassifier:
125
131
  frameworks = {framework.name for framework in stack.frameworks}
126
132
  if frameworks & _WEB_FRAMEWORKS or stack.stack == "nodejs":
127
133
  has_web = True
128
- if frameworks & _API_FRAMEWORKS or stack.stack in {"python", "go", "java", "php", "ruby"}:
134
+ if frameworks & _API_FRAMEWORKS or stack.stack in _API_STACKS:
129
135
  has_api = True
130
136
  return has_web and has_api
131
137
 
@@ -139,9 +145,9 @@ class TypeClassifier:
139
145
  score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
140
146
  manifest_weight = 1 if stack.manifests else 0
141
147
  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(
148
+ if project_type in {"webapp", "fullstack"} and stack.stack in {"nodejs", "elixir"} or project_type == "api" and stack.stack in _API_STACKS or project_type == "cli" and any(
143
149
  framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
144
- ):
150
+ ) or project_type == "cli" and stack.stack in {"cpp", "dotnet"}:
145
151
  priority = 3
146
152
  return (priority, score, manifest_weight)
147
153
 
@@ -157,6 +163,12 @@ class TypeClassifier:
157
163
  "php": (".php",),
158
164
  "ruby": (".rb",),
159
165
  "dart": (".dart",),
166
+ "dotnet": (".cs", ".fs"),
167
+ "elixir": (".ex", ".exs"),
168
+ "kotlin": (".kt", ".kts"),
169
+ "scala": (".scala",),
170
+ "terraform": (".tf",),
171
+ "cpp": (".c", ".cc", ".cpp", ".cxx", ".hpp", ".h"),
160
172
  }.get(stack, ())
161
173
  return sum(1 for path in flatten_file_tree(file_tree) if path.endswith(extensions))
162
174
 
@@ -8,15 +8,20 @@ from sourcecode.detectors.base import (
8
8
  StackDetection,
9
9
  )
10
10
  from sourcecode.detectors.dart import DartDetector
11
+ from sourcecode.detectors.dotnet import DotnetDetector
12
+ from sourcecode.detectors.elixir import ElixirDetector
11
13
  from sourcecode.detectors.go import GoDetector
12
14
  from sourcecode.detectors.heuristic import HeuristicDetector
13
15
  from sourcecode.detectors.java import JavaDetector
16
+ from sourcecode.detectors.jvm_ext import JvmExtDetector
14
17
  from sourcecode.detectors.nodejs import NodejsDetector
15
18
  from sourcecode.detectors.php import PhpDetector
16
19
  from sourcecode.detectors.project import ProjectDetector
17
20
  from sourcecode.detectors.python import PythonDetector
18
21
  from sourcecode.detectors.ruby import RubyDetector
19
22
  from sourcecode.detectors.rust import RustDetector
23
+ from sourcecode.detectors.systems import SystemsDetector
24
+ from sourcecode.detectors.terraform import TerraformDetector
20
25
 
21
26
 
22
27
  def build_default_detectors() -> list[AbstractDetector]:
@@ -27,9 +32,14 @@ def build_default_detectors() -> list[AbstractDetector]:
27
32
  GoDetector(),
28
33
  RustDetector(),
29
34
  JavaDetector(),
35
+ DotnetDetector(),
36
+ ElixirDetector(),
37
+ JvmExtDetector(),
30
38
  PhpDetector(),
31
39
  RubyDetector(),
32
40
  DartDetector(),
41
+ TerraformDetector(),
42
+ SystemsDetector(),
33
43
  HeuristicDetector(),
34
44
  ]
35
45
 
@@ -39,11 +49,14 @@ __all__ = [
39
49
  "build_default_detectors",
40
50
  "DartDetector",
41
51
  "DetectionContext",
52
+ "DotnetDetector",
53
+ "ElixirDetector",
42
54
  "EntryPoint",
43
55
  "FrameworkDetection",
44
56
  "GoDetector",
45
57
  "HeuristicDetector",
46
58
  "JavaDetector",
59
+ "JvmExtDetector",
47
60
  "NodejsDetector",
48
61
  "PhpDetector",
49
62
  "PythonDetector",
@@ -51,4 +64,6 @@ __all__ = [
51
64
  "RubyDetector",
52
65
  "RustDetector",
53
66
  "StackDetection",
67
+ "SystemsDetector",
68
+ "TerraformDetector",
54
69
  ]
@@ -0,0 +1,58 @@
1
+ """Detector de proyectos .NET / C#."""
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, path_exists_in_tree
13
+
14
+
15
+ class DotnetDetector(AbstractDetector):
16
+ name = "dotnet"
17
+ priority = 32
18
+
19
+ def can_detect(self, context: DetectionContext) -> bool:
20
+ return any(
21
+ path.endswith((".csproj", ".sln"))
22
+ for path in flatten_file_tree(context.file_tree)
23
+ )
24
+
25
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
26
+ manifests = [
27
+ path
28
+ for path in flatten_file_tree(context.file_tree)
29
+ if path.endswith((".csproj", ".sln"))
30
+ ]
31
+ content = "\n".join(
32
+ "\n".join(read_text_lines(context.root / manifest))
33
+ for manifest in manifests
34
+ ).lower()
35
+
36
+ frameworks: list[FrameworkDetection] = []
37
+ if "microsoft.net.sdk.web" in content or "microsoft.aspnetcore" in content:
38
+ frameworks.append(FrameworkDetection(name="ASP.NET Core", source="manifest"))
39
+
40
+ entry_points: list[EntryPoint] = []
41
+ for path in unique_strings(
42
+ ["Program.cs", "src/Program.cs", "Program.fs", "src/Program.fs"]
43
+ ):
44
+ if path_exists_in_tree(context.file_tree, path):
45
+ kind = "api" if frameworks else "cli"
46
+ entry_points.append(
47
+ EntryPoint(path=path, stack="dotnet", kind=kind, source="manifest")
48
+ )
49
+
50
+ stack = StackDetection(
51
+ stack="dotnet",
52
+ detection_method="manifest",
53
+ confidence="high",
54
+ frameworks=frameworks,
55
+ package_manager="nuget",
56
+ manifests=manifests,
57
+ )
58
+ return [stack], entry_points
@@ -0,0 +1,49 @@
1
+ """Detector de proyectos Elixir / Phoenix."""
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, path_exists_in_tree
13
+
14
+
15
+ class ElixirDetector(AbstractDetector):
16
+ name = "elixir"
17
+ priority = 33
18
+
19
+ def can_detect(self, context: DetectionContext) -> bool:
20
+ return path_exists_in_tree(context.file_tree, "mix.exs")
21
+
22
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
23
+ content = "\n".join(read_text_lines(context.root / "mix.exs")).lower()
24
+ frameworks: list[FrameworkDetection] = []
25
+ if "phoenix" in content:
26
+ frameworks.append(FrameworkDetection(name="Phoenix", source="mix.exs"))
27
+
28
+ candidates = [
29
+ path
30
+ for path in flatten_file_tree(context.file_tree)
31
+ if path.endswith("/application.ex")
32
+ ]
33
+ entry_points: list[EntryPoint] = []
34
+ for path in unique_strings(candidates + ["mix.exs"]):
35
+ if path_exists_in_tree(context.file_tree, path):
36
+ kind = "web" if frameworks else "app"
37
+ entry_points.append(
38
+ EntryPoint(path=path, stack="elixir", kind=kind, source="mix.exs")
39
+ )
40
+
41
+ stack = StackDetection(
42
+ stack="elixir",
43
+ detection_method="manifest",
44
+ confidence="high",
45
+ frameworks=frameworks,
46
+ package_manager="mix",
47
+ manifests=["mix.exs"],
48
+ )
49
+ return [stack], entry_points
@@ -0,0 +1,69 @@
1
+ """Detector JVM ampliado para Kotlin y Scala."""
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, path_exists_in_tree
13
+
14
+
15
+ class JvmExtDetector(AbstractDetector):
16
+ name = "jvm_ext"
17
+ priority = 36
18
+
19
+ def can_detect(self, context: DetectionContext) -> bool:
20
+ flat_paths = flatten_file_tree(context.file_tree)
21
+ return any(
22
+ path in {"build.gradle.kts", "settings.gradle.kts", "build.sbt"}
23
+ for path in flat_paths
24
+ )
25
+
26
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
27
+ flat_paths = flatten_file_tree(context.file_tree)
28
+ manifests = [
29
+ path
30
+ for path in flat_paths
31
+ if path in {"build.gradle.kts", "settings.gradle.kts", "build.sbt"}
32
+ ]
33
+ content = "\n".join(
34
+ "\n".join(read_text_lines(context.root / manifest))
35
+ for manifest in manifests
36
+ ).lower()
37
+
38
+ stack_name = "scala" if "build.sbt" in manifests or any(path.endswith(".scala") for path in flat_paths) else "kotlin"
39
+ frameworks: list[FrameworkDetection] = []
40
+ if "spring-boot" in content:
41
+ frameworks.append(FrameworkDetection(name="Spring Boot", source="manifest"))
42
+ if "ktor" in content:
43
+ frameworks.append(FrameworkDetection(name="Ktor", source="manifest"))
44
+ if "play" in content and stack_name == "scala":
45
+ frameworks.append(FrameworkDetection(name="Play", source="manifest"))
46
+
47
+ entry_candidates = [
48
+ "src/main/kotlin/Application.kt",
49
+ "src/main/kotlin/Main.kt",
50
+ "src/main/scala/Main.scala",
51
+ "src/main/scala/Application.scala",
52
+ ]
53
+ entry_points: list[EntryPoint] = []
54
+ for path in unique_strings(entry_candidates):
55
+ if path_exists_in_tree(context.file_tree, path):
56
+ kind = "api" if frameworks else "app"
57
+ entry_points.append(
58
+ EntryPoint(path=path, stack=stack_name, kind=kind, source="manifest")
59
+ )
60
+
61
+ stack = StackDetection(
62
+ stack=stack_name,
63
+ detection_method="manifest",
64
+ confidence="high",
65
+ frameworks=frameworks,
66
+ package_manager="gradle" if stack_name == "kotlin" else "sbt",
67
+ manifests=manifests,
68
+ )
69
+ return [stack], entry_points
@@ -65,6 +65,7 @@ class NodejsDetector(AbstractDetector):
65
65
 
66
66
  def _detect_package_manager(self, context: DetectionContext) -> str | None:
67
67
  for filename, package_manager in (
68
+ ("bun.lockb", "bun"),
68
69
  ("pnpm-lock.yaml", "pnpm"),
69
70
  ("package-lock.json", "npm"),
70
71
  ("yarn.lock", "yarn"),
@@ -7,6 +7,7 @@ from typing import Any
7
7
 
8
8
  from sourcecode.classifier import TypeClassifier
9
9
  from sourcecode.detectors.base import AbstractDetector, DetectionContext
10
+ from sourcecode.detectors.tooling import collect_tooling_signals, infer_package_manager
10
11
  from sourcecode.schema import EntryPoint, FrameworkDetection, StackDetection
11
12
 
12
13
  _CONFIDENCE_RANK = {"low": 0, "medium": 1, "high": 2}
@@ -45,6 +46,12 @@ class ProjectDetector:
45
46
  copied = self._copy_stack(stack)
46
47
  if copied.root is None:
47
48
  copied.root = "."
49
+ copied.signals = self._merge_values(
50
+ copied.signals,
51
+ collect_tooling_signals(file_tree),
52
+ )
53
+ if copied.package_manager is None:
54
+ copied.package_manager = infer_package_manager(copied.stack, file_tree)
48
55
  merged_stacks[stack.stack] = copied
49
56
  continue
50
57
  merged_stacks[stack.stack] = self._merge_stack(existing, stack)
@@ -93,6 +100,7 @@ class ProjectDetector:
93
100
  def _merge_stack(self, current: StackDetection, incoming: StackDetection) -> StackDetection:
94
101
  current.frameworks = self._merge_frameworks(current.frameworks, incoming.frameworks)
95
102
  current.manifests = self._merge_manifests(current.manifests, incoming.manifests)
103
+ current.signals = self._merge_values(current.signals, incoming.signals)
96
104
  if incoming.package_manager and not current.package_manager:
97
105
  current.package_manager = incoming.package_manager
98
106
  if self._confidence_rank(incoming.confidence) > self._confidence_rank(current.confidence):
@@ -117,6 +125,9 @@ class ProjectDetector:
117
125
  return list(merged.values())
118
126
 
119
127
  def _merge_manifests(self, current: Iterable[str], incoming: Iterable[str]) -> list[str]:
128
+ return self._merge_values(current, incoming)
129
+
130
+ def _merge_values(self, current: Iterable[str], incoming: Iterable[str]) -> list[str]:
120
131
  merged: list[str] = []
121
132
  seen: set[str] = set()
122
133
  for manifest in list(current) + list(incoming):
@@ -28,8 +28,19 @@ class PythonDetector(AbstractDetector):
28
28
  priority = 30
29
29
 
30
30
  def can_detect(self, context: DetectionContext) -> bool:
31
- supported = {"pyproject.toml", "requirements.txt", "setup.py", "Pipfile", "uv.lock"}
32
- return any(manifest in supported for manifest in context.manifests)
31
+ supported = {
32
+ "pyproject.toml",
33
+ "requirements.txt",
34
+ "setup.py",
35
+ "Pipfile",
36
+ "Pipfile.lock",
37
+ "uv.lock",
38
+ "poetry.lock",
39
+ }
40
+ return any(manifest in supported for manifest in context.manifests) or any(
41
+ path_exists_in_tree(context.file_tree, path)
42
+ for path in ("poetry.lock", "Pipfile.lock")
43
+ )
33
44
 
34
45
  def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
35
46
  dependencies = self._collect_dependencies(context)
@@ -42,15 +53,23 @@ class PythonDetector(AbstractDetector):
42
53
 
43
54
  manifests = [
44
55
  manifest
45
- for manifest in ("pyproject.toml", "requirements.txt", "setup.py", "Pipfile", "uv.lock")
46
- if manifest in context.manifests
56
+ for manifest in (
57
+ "pyproject.toml",
58
+ "requirements.txt",
59
+ "setup.py",
60
+ "Pipfile",
61
+ "Pipfile.lock",
62
+ "uv.lock",
63
+ "poetry.lock",
64
+ )
65
+ if manifest in context.manifests or path_exists_in_tree(context.file_tree, manifest)
47
66
  ]
48
67
  stack = StackDetection(
49
68
  stack="python",
50
69
  detection_method="manifest",
51
70
  confidence="high" if "pyproject.toml" in manifests else "medium",
52
71
  frameworks=frameworks,
53
- package_manager="pip",
72
+ package_manager=self._detect_package_manager(context, pyproject=load_toml_file(context.root / "pyproject.toml")),
54
73
  manifests=manifests,
55
74
  )
56
75
  return [stack], entry_points
@@ -67,6 +86,22 @@ class PythonDetector(AbstractDetector):
67
86
  for group in optional.values():
68
87
  if isinstance(group, list):
69
88
  deps.update(self._normalize_requirement(dep) for dep in group)
89
+ tool = pyproject.get("tool", {})
90
+ if isinstance(tool, dict):
91
+ poetry = tool.get("poetry", {})
92
+ if isinstance(poetry, dict):
93
+ deps.update(
94
+ self._normalize_requirement(dep_name)
95
+ for dep_name in poetry.get("dependencies", {})
96
+ )
97
+ groups = poetry.get("group", {})
98
+ if isinstance(groups, dict):
99
+ for group in groups.values():
100
+ if isinstance(group, dict):
101
+ deps.update(
102
+ self._normalize_requirement(dep_name)
103
+ for dep_name in group.get("dependencies", {})
104
+ )
70
105
 
71
106
  deps.update(self._read_requirements(context.root / "requirements.txt"))
72
107
  deps.update(self._read_requirements(context.root / "Pipfile"))
@@ -118,3 +153,20 @@ class PythonDetector(AbstractDetector):
118
153
  EntryPoint(path=path, stack="python", kind=kind, source="manifest")
119
154
  )
120
155
  return entry_points
156
+
157
+ def _detect_package_manager(
158
+ self, context: DetectionContext, *, pyproject: dict[str, Any] | None
159
+ ) -> str:
160
+ if path_exists_in_tree(context.file_tree, "poetry.lock"):
161
+ return "poetry"
162
+ if path_exists_in_tree(context.file_tree, "Pipfile.lock") or path_exists_in_tree(
163
+ context.file_tree, "Pipfile"
164
+ ):
165
+ return "pipenv"
166
+ if path_exists_in_tree(context.file_tree, "uv.lock"):
167
+ return "uv"
168
+ if pyproject:
169
+ tool = pyproject.get("tool", {})
170
+ if isinstance(tool, dict) and "poetry" in tool:
171
+ return "poetry"
172
+ return "pip"
@@ -0,0 +1,41 @@
1
+ """Detector de proyectos C / C++ y build systems asociados."""
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 unique_strings
11
+ from sourcecode.tree_utils import flatten_file_tree, path_exists_in_tree
12
+
13
+
14
+ class SystemsDetector(AbstractDetector):
15
+ name = "systems"
16
+ priority = 45
17
+
18
+ def can_detect(self, context: DetectionContext) -> bool:
19
+ flat_paths = flatten_file_tree(context.file_tree)
20
+ has_code = any(path.endswith((".c", ".cc", ".cpp", ".cxx", ".hpp", ".h")) for path in flat_paths)
21
+ has_build = any(path in {"CMakeLists.txt", "Makefile", "meson.build"} for path in flat_paths)
22
+ return has_code and has_build
23
+
24
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
25
+ flat_paths = flatten_file_tree(context.file_tree)
26
+ manifests = [path for path in flat_paths if path in {"CMakeLists.txt", "Makefile", "meson.build"}]
27
+ entry_points: list[EntryPoint] = []
28
+ for path in unique_strings(["main.c", "main.cpp", "src/main.c", "src/main.cpp"]):
29
+ if path_exists_in_tree(context.file_tree, path):
30
+ entry_points.append(
31
+ EntryPoint(path=path, stack="cpp", kind="cli", source="manifest")
32
+ )
33
+
34
+ stack = StackDetection(
35
+ stack="cpp",
36
+ detection_method="manifest",
37
+ confidence="medium",
38
+ package_manager="cmake" if "CMakeLists.txt" in manifests else None,
39
+ manifests=manifests,
40
+ )
41
+ return [stack], entry_points
@@ -0,0 +1,47 @@
1
+ """Detector de proyectos Terraform."""
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.tree_utils import flatten_file_tree, path_exists_in_tree
12
+
13
+
14
+ class TerraformDetector(AbstractDetector):
15
+ name = "terraform"
16
+ priority = 40
17
+
18
+ def can_detect(self, context: DetectionContext) -> bool:
19
+ return any(path.endswith(".tf") for path in flatten_file_tree(context.file_tree))
20
+
21
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
22
+ manifests = [path for path in flatten_file_tree(context.file_tree) if path.endswith(".tf")]
23
+ signals: list[str] = []
24
+ for manifest in manifests:
25
+ content = "\n".join(read_text_lines(context.root / manifest)).lower()
26
+ if 'provider "aws"' in content:
27
+ signals.append("provider:aws")
28
+ if 'provider "azurerm"' in content:
29
+ signals.append("provider:azure")
30
+ if 'provider "google"' in content:
31
+ signals.append("provider:gcp")
32
+
33
+ entry_points: list[EntryPoint] = []
34
+ if path_exists_in_tree(context.file_tree, "main.tf"):
35
+ entry_points.append(
36
+ EntryPoint(path="main.tf", stack="terraform", kind="infra", source="manifest")
37
+ )
38
+
39
+ stack = StackDetection(
40
+ stack="terraform",
41
+ detection_method="manifest",
42
+ confidence="medium",
43
+ package_manager="terraform",
44
+ manifests=manifests,
45
+ signals=signals,
46
+ )
47
+ return [stack], entry_points
@@ -0,0 +1,53 @@
1
+ """Senales universales de tooling y helpers transversales."""
2
+ from __future__ import annotations
3
+
4
+ from sourcecode.tree_utils import flatten_file_tree
5
+
6
+
7
+ def collect_tooling_signals(file_tree: dict[str, object]) -> list[str]:
8
+ """Recolecta senales operativas reutilizables sin crear stacks por si solas."""
9
+ flat_paths = set(flatten_file_tree(file_tree))
10
+ signals: list[str] = []
11
+ for path, label in (
12
+ ("Dockerfile", "tooling:docker"),
13
+ ("docker-compose.yml", "tooling:docker-compose"),
14
+ ("docker-compose.yaml", "tooling:docker-compose"),
15
+ ("Taskfile.yml", "tooling:taskfile"),
16
+ ("Taskfile.yaml", "tooling:taskfile"),
17
+ ("justfile", "tooling:just"),
18
+ ("Procfile", "tooling:procfile"),
19
+ ("Makefile", "tooling:make"),
20
+ ):
21
+ if path in flat_paths:
22
+ signals.append(label)
23
+ return signals
24
+
25
+
26
+ def infer_package_manager(stack: str, file_tree: dict[str, object]) -> str | None:
27
+ """Infiera package manager por lockfiles o tooling conocido."""
28
+ flat_paths = set(flatten_file_tree(file_tree))
29
+ if stack == "nodejs":
30
+ for path, manager in (
31
+ ("bun.lockb", "bun"),
32
+ ("pnpm-lock.yaml", "pnpm"),
33
+ ("package-lock.json", "npm"),
34
+ ("yarn.lock", "yarn"),
35
+ ):
36
+ if path in flat_paths:
37
+ return manager
38
+ if stack == "python":
39
+ for path, manager in (
40
+ ("poetry.lock", "poetry"),
41
+ ("Pipfile.lock", "pipenv"),
42
+ ("uv.lock", "uv"),
43
+ ("requirements.txt", "pip"),
44
+ ):
45
+ if path in flat_paths:
46
+ return manager
47
+ if stack == "php" and "composer.lock" in flat_paths:
48
+ return "composer"
49
+ if stack == "ruby" and "Gemfile.lock" in flat_paths:
50
+ return "bundler"
51
+ if stack == "terraform" and any(path.endswith(".tf") for path in flat_paths):
52
+ return "terraform"
53
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -16,61 +16,61 @@ Description-Content-Type: text/markdown
16
16
 
17
17
  # sourcecode
18
18
 
19
- `sourcecode` genera un mapa de contexto estructurado de un proyecto de software para que un agente pueda entender rapido su stack, puntos de entrada y forma del repositorio.
19
+ `sourcecode` generates a structured project context map so an agent can quickly understand a repository's stack, entry points, and overall shape.
20
20
 
21
- ## Instalacion
21
+ ## Installation
22
22
 
23
23
  ```bash
24
24
  pip install sourcecode
25
25
  ```
26
26
 
27
- Requiere Python `3.9+`.
27
+ Requires Python `3.9+`.
28
28
 
29
- ## Uso rapido
29
+ ## Quick Start
30
30
 
31
- Analizar el directorio actual en JSON:
31
+ Analyze the current directory as JSON:
32
32
 
33
33
  ```bash
34
34
  sourcecode .
35
35
  ```
36
36
 
37
- Generar una vista compacta para prompts o handoff:
37
+ Generate a compact view for prompts or handoff:
38
38
 
39
39
  ```bash
40
40
  sourcecode --compact .
41
41
  ```
42
42
 
43
- Analizar otro directorio y escribir YAML a fichero:
43
+ Analyze another directory and write YAML to a file:
44
44
 
45
45
  ```bash
46
- sourcecode --format yaml --output sourcecode.yaml /ruta/al/proyecto
46
+ sourcecode --format yaml --output sourcecode.yaml /path/to/project
47
47
  ```
48
48
 
49
- Mostrar version:
49
+ Show the version:
50
50
 
51
51
  ```bash
52
52
  sourcecode --version
53
53
  ```
54
54
 
55
- Opciones principales:
55
+ Main options:
56
56
 
57
- - `--format json|yaml`: formato de salida.
58
- - `--output PATH`: escribe a fichero en vez de `stdout`.
59
- - `--compact`: devuelve una vista reducida con `schema_version`, `project_type`, `stacks`, `entry_points` y `file_tree_depth1`.
60
- - `--depth INTEGER`: profundidad maxima del arbol de ficheros.
61
- - `--no-redact`: desactiva la redaccion de secretos.
57
+ - `--format json|yaml`: output format.
58
+ - `--output PATH`: write to a file instead of `stdout`.
59
+ - `--compact`: return a reduced view with `schema_version`, `project_type`, `stacks`, `entry_points`, and `file_tree_depth1`.
60
+ - `--depth INTEGER`: maximum file tree depth.
61
+ - `--no-redact`: disable secret redaction.
62
62
 
63
- ## Que detecta
63
+ ## What It Detects
64
64
 
65
- - Stacks: Node.js, Python, Go, Rust, Java, PHP, Ruby y Dart.
66
- - Frameworks asociados al stack cuando hay senales suficientes.
67
- - `project_type`: `webapp`, `api`, `library`, `cli`, `fullstack`, `monorepo` o `unknown`.
68
- - `entry_points` relevantes, como `main.py`, `cmd/api/main.go` o `app/page.tsx`.
69
- - Workspaces y roots por stack en repos multi-stack o monorepo.
65
+ - Stacks: Node.js, Python, Go, Rust, Java, PHP, Ruby, and Dart.
66
+ - Frameworks associated with each stack when enough signals are present.
67
+ - `project_type`: `webapp`, `api`, `library`, `cli`, `fullstack`, `monorepo`, or `unknown`.
68
+ - Relevant `entry_points`, such as `main.py`, `cmd/api/main.go`, or `app/page.tsx`.
69
+ - Workspace roots in multi-stack or monorepo repositories.
70
70
 
71
- ## Ejemplo compacto
71
+ ## Compact Example
72
72
 
73
- Salida real de un fixture Next.js:
73
+ Real output from a Next.js fixture:
74
74
 
75
75
  ```json
76
76
  {
@@ -115,9 +115,9 @@ Salida real de un fixture Next.js:
115
115
  }
116
116
  ```
117
117
 
118
- ## Ejemplo monorepo
118
+ ## Monorepo Example
119
119
 
120
- En un monorepo, cada stack incluye su `root` y `workspace`, y uno de ellos queda marcado como `primary`.
120
+ In a monorepo, each stack includes its own `root` and `workspace`, and one of them is marked as `primary`.
121
121
 
122
122
  ```json
123
123
  {
@@ -145,25 +145,25 @@ En un monorepo, cada stack incluye su `root` y `workspace`, y uno de ellos queda
145
145
 
146
146
  ## Output
147
147
 
148
- El schema completo incluye:
148
+ The full schema includes:
149
149
 
150
- - `metadata`: version de schema, timestamp, version de `sourcecode` y ruta analizada.
151
- - `file_tree`: arbol del repositorio donde `null` representa un fichero y un objeto representa un directorio.
152
- - `stacks`: detecciones por ecosistema con confianza, frameworks, manifests, `primary`, `root`, `workspace` y `signals`.
153
- - `project_type`: clasificacion general del proyecto.
154
- - `entry_points`: entradas detectadas por stack.
150
+ - `metadata`: schema version, timestamp, `sourcecode` version, and analyzed path.
151
+ - `file_tree`: repository tree where `null` represents a file and an object represents a directory.
152
+ - `stacks`: stack detections with confidence, frameworks, manifests, `primary`, `root`, `workspace`, and `signals`.
153
+ - `project_type`: overall project classification.
154
+ - `entry_points`: detected entry points by stack.
155
155
 
156
- La referencia detallada esta en [docs/schema.md](/Users/user/Documents/workspace/atlas/atlas-cli/docs/schema.md).
156
+ Detailed reference: [docs/schema.md](/Users/user/Documents/workspace/atlas/atlas-cli/docs/schema.md).
157
157
 
158
- ## Desarrollo
158
+ ## Development
159
159
 
160
- Instalacion editable con dependencias de desarrollo:
160
+ Editable install with development dependencies:
161
161
 
162
162
  ```bash
163
163
  pip install -e ".[dev]"
164
164
  ```
165
165
 
166
- Validacion local:
166
+ Local validation:
167
167
 
168
168
  ```bash
169
169
  ruff check src tests
@@ -1,5 +1,5 @@
1
- sourcecode/__init__.py,sha256=zFh8ACgMMXbAd5f8rHwMQHBQ6irzFx0iQ1NkQc34ff8,99
2
- sourcecode/classifier.py,sha256=GgJE9DjsMkXgK8DIprr1cfa_FiQBZsd_4ZxzyGYw1b8,6405
1
+ sourcecode/__init__.py,sha256=kSF53Kglqc3H1-xnzSg40MVrMtrK-Fq-mxBVtQDGJig,99
2
+ sourcecode/classifier.py,sha256=7ubGTn5vfw9z-9eosLIYDtQUu7d8dvB4XKKDxysUVSM,6910
3
3
  sourcecode/cli.py,sha256=sf1nf-BmPWzscFvoScNkKIlfU10wepOgTfFE-lf4NO4,6962
4
4
  sourcecode/redactor.py,sha256=abSf5jH_IzYzpjF3lEN6hCwdXxvmyHs-3DHz9fd8Mic,2883
5
5
  sourcecode/scanner.py,sha256=TvcuD77m_NF8OlTj3XH1b_jO0qrto8c90fND96ecqb0,6182
@@ -7,20 +7,26 @@ sourcecode/schema.py,sha256=E2_EnuYWHWZOLarxENvL5S66QBkZIpyV1BzKetCJWIY,2487
7
7
  sourcecode/serializer.py,sha256=hezTYOAAvZ2yPn7Te7Eu42ajbMhVCsUkuUZtqx4wjAs,3085
8
8
  sourcecode/tree_utils.py,sha256=5WHQc2L-AGqxKFfYYcQftE6hETxgUxP9MWDuaThcAUw,991
9
9
  sourcecode/workspace.py,sha256=1L4xH38gU89fLeMYSuF3ft7lSdCyTfMfJ9NQt5q4Ric,6904
10
- sourcecode/detectors/__init__.py,sha256=RMSr1E3uIK7MvfAQdVo4lfkcDptW3tBzLPFZwQju6C4,1455
10
+ sourcecode/detectors/__init__.py,sha256=A0AACJFF6HWf_RgatNtWu3PUzstcKtIGM9f1PoFcJug,1987
11
11
  sourcecode/detectors/base.py,sha256=mLqDNil8VkFRNbzwO2wPbmw_aaVg3WR_WzzRjU58xbU,1070
12
12
  sourcecode/detectors/dart.py,sha256=77KDgBY-xkbzLjs0Emw6c0NuQY-tQZR5PCHa9r0t2OE,1476
13
+ sourcecode/detectors/dotnet.py,sha256=BofpMPVOJmiUpCFpkd0y7zSzzBj4jeXZkZR5AksBg8w,2017
14
+ sourcecode/detectors/elixir.py,sha256=Aurq99BvJvZ1k9lNBiI-RGOJzi8e7maQd225IG128PE,1759
13
15
  sourcecode/detectors/go.py,sha256=oifvbih6i4yrUO2zQs3kVXEdirr6jVriiV1fJ_-NF2I,1748
14
16
  sourcecode/detectors/heuristic.py,sha256=Phimturkvi5ZwNdt005TizYTrllZk1pqHpfXOkFZJbc,1943
15
17
  sourcecode/detectors/java.py,sha256=XdONBlcBb0Vo-2hvG77Fw00X41d2KlHFCPXJCuzwAl0,3547
16
- sourcecode/detectors/nodejs.py,sha256=2LDNoqrQcOm_ne66PKdIDBJR_7XEFTfB42Luhz2IoEg,3885
18
+ sourcecode/detectors/jvm_ext.py,sha256=ViHKwUzlKXjpjNNB6_wK0XptR29VUsdLobW2U-mS5BM,2665
19
+ sourcecode/detectors/nodejs.py,sha256=rfUG2oT5SsV5nMCuXY4v99IrAWF0lL5i47m9RHOM2mM,3919
17
20
  sourcecode/detectors/parsers.py,sha256=CQhhFGk8lSXruIfLe571-1HOId3W0ssnY2ioHMGtOrg,2338
18
21
  sourcecode/detectors/php.py,sha256=6l5pFlUuKrrC6XlWPSgYTEnnPm3hoFxMM4DRAnjL4nM,1920
19
- sourcecode/detectors/project.py,sha256=Fu3N_5ZKtv5b0F7i8Om36k_SZKvm24nRURkHyLogp6Q,5379
20
- sourcecode/detectors/python.py,sha256=DMrlKXapsaRu8jVmO-w-FfnFVXTmQnmpQTaV2PBvIZA,4875
22
+ sourcecode/detectors/project.py,sha256=QOTt2p55Wt6FNUSo0_1P7iojM_CsVevvQqHQmLqVxYk,6022
23
+ sourcecode/detectors/python.py,sha256=UVB5M_B8t-zQkSrIqmNQR5KD7RqDHzT8C3CcazgxEi8,6900
21
24
  sourcecode/detectors/ruby.py,sha256=3_Hd5YqlErpTtyTRcUTPnMp1TF92hT-TnYYeclLbF1c,1625
22
25
  sourcecode/detectors/rust.py,sha256=iUz9lE4HfBy8CBaDwGhfh6kLsNIovrH2ay2v3y6qNDc,2298
23
- sourcecode-0.1.0.dist-info/METADATA,sha256=vCBUxFWpWLh7zfzltWS3LzBsT019LuhlNz89Lr-15aI,4179
24
- sourcecode-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
25
- sourcecode-0.1.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
26
- sourcecode-0.1.0.dist-info/RECORD,,
26
+ sourcecode/detectors/systems.py,sha256=qf5M9tio1H6XDorSstTWIxKe0xVLR_QGTJpFGyhDz4A,1690
27
+ sourcecode/detectors/terraform.py,sha256=5EOKQtTViWo7u3tFK0h45YD8GQPwhrww0cOicxGPPn4,1732
28
+ sourcecode/detectors/tooling.py,sha256=wPvudLCJZ8_cr9GIx44C8jkLAlEN9uTUZcJsP7eVYnQ,1937
29
+ sourcecode-0.2.0.dist-info/METADATA,sha256=kfVit3IrZnWnPAtbKcLjMK3SXCfHbNlSP8JrxB6zI0o,4086
30
+ sourcecode-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
31
+ sourcecode-0.2.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
32
+ sourcecode-0.2.0.dist-info/RECORD,,