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 +3 -0
- sourcecode/classifier.py +177 -0
- sourcecode/cli.py +208 -0
- sourcecode/detectors/__init__.py +54 -0
- sourcecode/detectors/base.py +42 -0
- sourcecode/detectors/dart.py +42 -0
- sourcecode/detectors/go.py +52 -0
- sourcecode/detectors/heuristic.py +70 -0
- sourcecode/detectors/java.py +88 -0
- sourcecode/detectors/nodejs.py +112 -0
- sourcecode/detectors/parsers.py +73 -0
- sourcecode/detectors/php.py +58 -0
- sourcecode/detectors/project.py +129 -0
- sourcecode/detectors/python.py +120 -0
- sourcecode/detectors/ruby.py +44 -0
- sourcecode/detectors/rust.py +69 -0
- sourcecode/redactor.py +81 -0
- sourcecode/scanner.py +180 -0
- sourcecode/schema.py +87 -0
- sourcecode/serializer.py +90 -0
- sourcecode/tree_utils.py +28 -0
- sourcecode/workspace.py +166 -0
- sourcecode-0.1.0.dist-info/METADATA +172 -0
- sourcecode-0.1.0.dist-info/RECORD +26 -0
- sourcecode-0.1.0.dist-info/WHEEL +4 -0
- sourcecode-0.1.0.dist-info/entry_points.txt +2 -0
sourcecode/workspace.py
ADDED
|
@@ -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,,
|