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.
@@ -0,0 +1,166 @@
1
+ """Descubrimiento de workspaces y señales de monorepo."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from sourcecode.scanner import DEFAULT_EXCLUDES
8
+
9
+
10
+ @dataclass
11
+ class WorkspaceCandidate:
12
+ """Subdirectorio candidato a workspace."""
13
+
14
+ path: str
15
+ reason: str
16
+ depth: int
17
+
18
+
19
+ @dataclass
20
+ class WorkspaceAnalysis:
21
+ """Resultado del análisis de workspaces."""
22
+
23
+ is_monorepo: bool
24
+ markers: list[str] = field(default_factory=list)
25
+ workspaces: list[WorkspaceCandidate] = field(default_factory=list)
26
+
27
+
28
+ class WorkspaceAnalyzer:
29
+ """Detecta workspaces y señales de monorepo con recursión limitada."""
30
+
31
+ def __init__(self, max_depth: int = 3) -> None:
32
+ self.max_depth = max_depth
33
+
34
+ def analyze(self, root: Path, manifests: list[str]) -> WorkspaceAnalysis:
35
+ markers = self._detect_markers(root)
36
+ candidates: dict[str, WorkspaceCandidate] = {}
37
+
38
+ for candidate in self._workspace_candidates_from_manifests(root, manifests):
39
+ candidates[candidate.path] = candidate
40
+
41
+ for candidate in self._workspace_candidates_from_markers(root, markers):
42
+ candidates.setdefault(candidate.path, candidate)
43
+
44
+ workspaces = sorted(candidates.values(), key=lambda item: item.path)
45
+ return WorkspaceAnalysis(is_monorepo=bool(markers), markers=markers, workspaces=workspaces)
46
+
47
+ def _detect_markers(self, root: Path) -> list[str]:
48
+ markers: list[str] = []
49
+ for filename in ("pnpm-workspace.yaml", "go.work", "turbo.json", "lerna.json"):
50
+ if (root / filename).exists():
51
+ markers.append(filename)
52
+
53
+ cargo = root / "Cargo.toml"
54
+ if cargo.exists():
55
+ content = cargo.read_text(encoding="utf-8", errors="replace")
56
+ if "[workspace]" in content:
57
+ markers.append("Cargo.toml[workspace]")
58
+ return markers
59
+
60
+ def _workspace_candidates_from_manifests(
61
+ self, root: Path, manifests: list[str]
62
+ ) -> list[WorkspaceCandidate]:
63
+ candidates: list[WorkspaceCandidate] = []
64
+ for manifest in manifests:
65
+ try:
66
+ relative = Path(manifest).resolve().relative_to(root.resolve())
67
+ except ValueError:
68
+ continue
69
+ if len(relative.parts) <= 1:
70
+ continue
71
+ workspace = relative.parts[0]
72
+ if self._is_allowed_workspace(workspace):
73
+ candidates.append(
74
+ WorkspaceCandidate(path=workspace, reason=f"manifest:{relative.name}", depth=1)
75
+ )
76
+ return candidates
77
+
78
+ def _workspace_candidates_from_markers(
79
+ self, root: Path, markers: list[str]
80
+ ) -> list[WorkspaceCandidate]:
81
+ candidates: list[WorkspaceCandidate] = []
82
+ if "pnpm-workspace.yaml" in markers:
83
+ candidates.extend(self._from_pnpm_workspace(root))
84
+ if "go.work" in markers:
85
+ candidates.extend(self._from_go_work(root))
86
+ if "Cargo.toml[workspace]" in markers:
87
+ candidates.extend(self._from_cargo_workspace(root))
88
+ return [candidate for candidate in candidates if self._is_allowed_workspace(candidate.path)]
89
+
90
+ def _from_pnpm_workspace(self, root: Path) -> list[WorkspaceCandidate]:
91
+ content = (root / "pnpm-workspace.yaml").read_text(encoding="utf-8", errors="replace")
92
+ patterns = []
93
+ for line in content.splitlines():
94
+ stripped = line.strip().strip("'\"")
95
+ if stripped.startswith("- "):
96
+ patterns.append(stripped[2:].strip("'\""))
97
+
98
+ candidates: list[WorkspaceCandidate] = []
99
+ for pattern in patterns:
100
+ for path in self._resolve_pattern(root, pattern):
101
+ candidates.append(
102
+ WorkspaceCandidate(path=path, reason="marker:pnpm-workspace.yaml", depth=len(Path(path).parts))
103
+ )
104
+ return candidates
105
+
106
+ def _from_go_work(self, root: Path) -> list[WorkspaceCandidate]:
107
+ content = (root / "go.work").read_text(encoding="utf-8", errors="replace")
108
+ candidates: list[WorkspaceCandidate] = []
109
+ for line in content.splitlines():
110
+ stripped = line.strip()
111
+ if stripped.startswith("use "):
112
+ target = stripped.replace("use ", "", 1).strip().strip("()")
113
+ if target and target != ".":
114
+ rel = target.removeprefix("./").rstrip("/")
115
+ candidates.append(
116
+ WorkspaceCandidate(path=rel, reason="marker:go.work", depth=len(Path(rel).parts))
117
+ )
118
+ elif stripped.startswith("./"):
119
+ rel = stripped.removeprefix("./").rstrip()
120
+ candidates.append(
121
+ WorkspaceCandidate(path=rel, reason="marker:go.work", depth=len(Path(rel).parts))
122
+ )
123
+ return candidates
124
+
125
+ def _from_cargo_workspace(self, root: Path) -> list[WorkspaceCandidate]:
126
+ content = (root / "Cargo.toml").read_text(encoding="utf-8", errors="replace")
127
+ candidates: list[WorkspaceCandidate] = []
128
+ in_members = False
129
+ for line in content.splitlines():
130
+ stripped = line.strip()
131
+ if stripped.startswith("members"):
132
+ in_members = True
133
+ if in_members:
134
+ for quote in ('"', "'"):
135
+ if quote in stripped:
136
+ parts = [segment for segment in stripped.split(quote) if segment and segment not in {"[", "]", ",", "members = "}]
137
+ for part in parts:
138
+ if "/" in part or "*" in part:
139
+ for path in self._resolve_pattern(root, part):
140
+ candidates.append(
141
+ WorkspaceCandidate(path=path, reason="marker:Cargo.toml[workspace]", depth=len(Path(path).parts))
142
+ )
143
+ if "]" in stripped:
144
+ in_members = False
145
+ return candidates
146
+
147
+ def _resolve_pattern(self, root: Path, pattern: str) -> list[str]:
148
+ matches: list[str] = []
149
+ for candidate in root.glob(pattern):
150
+ if not candidate.is_dir():
151
+ continue
152
+ try:
153
+ relative = candidate.resolve().relative_to(root.resolve())
154
+ except ValueError:
155
+ continue
156
+ if 1 <= len(relative.parts) <= self.max_depth:
157
+ rel = str(relative)
158
+ if self._is_allowed_workspace(rel):
159
+ matches.append(rel)
160
+ return sorted(set(matches))
161
+
162
+ def _is_allowed_workspace(self, relative_path: str) -> bool:
163
+ parts = Path(relative_path).parts
164
+ if not parts or len(parts) > self.max_depth:
165
+ return False
166
+ return all(part not in DEFAULT_EXCLUDES for part in parts)
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: sourcecode
3
+ Version: 0.1.0
4
+ Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
5
+ License: MIT
6
+ Requires-Python: >=3.9
7
+ Requires-Dist: pathspec>=1.0
8
+ Requires-Dist: ruamel-yaml>=0.18
9
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
10
+ Requires-Dist: typer>=0.24
11
+ Provides-Extra: dev
12
+ Requires-Dist: mypy>=1.10; extra == 'dev'
13
+ Requires-Dist: pytest>=8; extra == 'dev'
14
+ Requires-Dist: ruff>=0.15; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # sourcecode
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.
20
+
21
+ ## Instalacion
22
+
23
+ ```bash
24
+ pip install sourcecode
25
+ ```
26
+
27
+ Requiere Python `3.9+`.
28
+
29
+ ## Uso rapido
30
+
31
+ Analizar el directorio actual en JSON:
32
+
33
+ ```bash
34
+ sourcecode .
35
+ ```
36
+
37
+ Generar una vista compacta para prompts o handoff:
38
+
39
+ ```bash
40
+ sourcecode --compact .
41
+ ```
42
+
43
+ Analizar otro directorio y escribir YAML a fichero:
44
+
45
+ ```bash
46
+ sourcecode --format yaml --output sourcecode.yaml /ruta/al/proyecto
47
+ ```
48
+
49
+ Mostrar version:
50
+
51
+ ```bash
52
+ sourcecode --version
53
+ ```
54
+
55
+ Opciones principales:
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.
62
+
63
+ ## Que detecta
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.
70
+
71
+ ## Ejemplo compacto
72
+
73
+ Salida real de un fixture Next.js:
74
+
75
+ ```json
76
+ {
77
+ "schema_version": "1.0",
78
+ "project_type": "webapp",
79
+ "stacks": [
80
+ {
81
+ "stack": "nodejs",
82
+ "detection_method": "manifest",
83
+ "confidence": "high",
84
+ "frameworks": [
85
+ { "name": "Next.js", "source": "package.json" },
86
+ { "name": "React", "source": "package.json" }
87
+ ],
88
+ "package_manager": "pnpm",
89
+ "manifests": ["package.json"],
90
+ "primary": true,
91
+ "root": ".",
92
+ "workspace": null,
93
+ "signals": [
94
+ "manifest:package.json",
95
+ "framework:Next.js",
96
+ "framework:React",
97
+ "package_manager:pnpm",
98
+ "entry:app/page.tsx"
99
+ ]
100
+ }
101
+ ],
102
+ "entry_points": [
103
+ {
104
+ "path": "app/page.tsx",
105
+ "stack": "nodejs",
106
+ "kind": "web",
107
+ "source": "package.json"
108
+ }
109
+ ],
110
+ "file_tree_depth1": {
111
+ "pnpm-lock.yaml": null,
112
+ "package.json": null,
113
+ "app": {}
114
+ }
115
+ }
116
+ ```
117
+
118
+ ## Ejemplo monorepo
119
+
120
+ En un monorepo, cada stack incluye su `root` y `workspace`, y uno de ellos queda marcado como `primary`.
121
+
122
+ ```json
123
+ {
124
+ "project_type": "monorepo",
125
+ "stacks": [
126
+ {
127
+ "stack": "nodejs",
128
+ "primary": true,
129
+ "root": "apps/web",
130
+ "workspace": "apps/web"
131
+ },
132
+ {
133
+ "stack": "python",
134
+ "primary": false,
135
+ "root": "packages/api",
136
+ "workspace": "packages/api"
137
+ }
138
+ ],
139
+ "entry_points": [
140
+ { "path": "apps/web/app/page.tsx", "stack": "nodejs", "kind": "web" },
141
+ { "path": "packages/api/main.py", "stack": "python", "kind": "cli" }
142
+ ]
143
+ }
144
+ ```
145
+
146
+ ## Output
147
+
148
+ El schema completo incluye:
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.
155
+
156
+ La referencia detallada esta en [docs/schema.md](/Users/user/Documents/workspace/atlas/atlas-cli/docs/schema.md).
157
+
158
+ ## Desarrollo
159
+
160
+ Instalacion editable con dependencias de desarrollo:
161
+
162
+ ```bash
163
+ pip install -e ".[dev]"
164
+ ```
165
+
166
+ Validacion local:
167
+
168
+ ```bash
169
+ ruff check src tests
170
+ mypy src
171
+ pytest -q
172
+ ```
@@ -0,0 +1,26 @@
1
+ sourcecode/__init__.py,sha256=zFh8ACgMMXbAd5f8rHwMQHBQ6irzFx0iQ1NkQc34ff8,99
2
+ sourcecode/classifier.py,sha256=GgJE9DjsMkXgK8DIprr1cfa_FiQBZsd_4ZxzyGYw1b8,6405
3
+ sourcecode/cli.py,sha256=sf1nf-BmPWzscFvoScNkKIlfU10wepOgTfFE-lf4NO4,6962
4
+ sourcecode/redactor.py,sha256=abSf5jH_IzYzpjF3lEN6hCwdXxvmyHs-3DHz9fd8Mic,2883
5
+ sourcecode/scanner.py,sha256=TvcuD77m_NF8OlTj3XH1b_jO0qrto8c90fND96ecqb0,6182
6
+ sourcecode/schema.py,sha256=E2_EnuYWHWZOLarxENvL5S66QBkZIpyV1BzKetCJWIY,2487
7
+ sourcecode/serializer.py,sha256=hezTYOAAvZ2yPn7Te7Eu42ajbMhVCsUkuUZtqx4wjAs,3085
8
+ sourcecode/tree_utils.py,sha256=5WHQc2L-AGqxKFfYYcQftE6hETxgUxP9MWDuaThcAUw,991
9
+ sourcecode/workspace.py,sha256=1L4xH38gU89fLeMYSuF3ft7lSdCyTfMfJ9NQt5q4Ric,6904
10
+ sourcecode/detectors/__init__.py,sha256=RMSr1E3uIK7MvfAQdVo4lfkcDptW3tBzLPFZwQju6C4,1455
11
+ sourcecode/detectors/base.py,sha256=mLqDNil8VkFRNbzwO2wPbmw_aaVg3WR_WzzRjU58xbU,1070
12
+ sourcecode/detectors/dart.py,sha256=77KDgBY-xkbzLjs0Emw6c0NuQY-tQZR5PCHa9r0t2OE,1476
13
+ sourcecode/detectors/go.py,sha256=oifvbih6i4yrUO2zQs3kVXEdirr6jVriiV1fJ_-NF2I,1748
14
+ sourcecode/detectors/heuristic.py,sha256=Phimturkvi5ZwNdt005TizYTrllZk1pqHpfXOkFZJbc,1943
15
+ sourcecode/detectors/java.py,sha256=XdONBlcBb0Vo-2hvG77Fw00X41d2KlHFCPXJCuzwAl0,3547
16
+ sourcecode/detectors/nodejs.py,sha256=2LDNoqrQcOm_ne66PKdIDBJR_7XEFTfB42Luhz2IoEg,3885
17
+ sourcecode/detectors/parsers.py,sha256=CQhhFGk8lSXruIfLe571-1HOId3W0ssnY2ioHMGtOrg,2338
18
+ 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
21
+ sourcecode/detectors/ruby.py,sha256=3_Hd5YqlErpTtyTRcUTPnMp1TF92hT-TnYYeclLbF1c,1625
22
+ 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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sourcecode = sourcecode.cli:app