sourcecode 0.20.0__py3-none-any.whl → 0.21.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 +1 -1
- sourcecode/cli.py +93 -17
- sourcecode/prepare_context.py +449 -160
- sourcecode/semantic_analyzer.py +2 -0
- {sourcecode-0.20.0.dist-info → sourcecode-0.21.0.dist-info}/METADATA +70 -1
- {sourcecode-0.20.0.dist-info → sourcecode-0.21.0.dist-info}/RECORD +8 -8
- {sourcecode-0.20.0.dist-info → sourcecode-0.21.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.20.0.dist-info → sourcecode-0.21.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -163,6 +163,10 @@ def main(
|
|
|
163
163
|
),
|
|
164
164
|
) -> None:
|
|
165
165
|
"""Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
|
|
166
|
+
# When a subcommand (e.g. prepare-context) is invoked, skip the main analysis.
|
|
167
|
+
if ctx.invoked_subcommand is not None:
|
|
168
|
+
return
|
|
169
|
+
|
|
166
170
|
# Validar formato
|
|
167
171
|
if format not in FORMAT_CHOICES:
|
|
168
172
|
typer.echo(
|
|
@@ -654,31 +658,103 @@ def main(
|
|
|
654
658
|
|
|
655
659
|
@app.command("prepare-context")
|
|
656
660
|
def prepare_context_cmd(
|
|
657
|
-
task: str = typer.Argument(
|
|
658
|
-
|
|
661
|
+
task: Optional[str] = typer.Argument(
|
|
662
|
+
None,
|
|
663
|
+
help="Task: explain | fix-bug | refactor | generate-tests",
|
|
664
|
+
),
|
|
665
|
+
path: Path = typer.Option(
|
|
666
|
+
Path("."),
|
|
667
|
+
"--path", "-p",
|
|
668
|
+
help="Project directory to analyze (default: current directory)",
|
|
669
|
+
),
|
|
670
|
+
llm_prompt: bool = typer.Option(
|
|
671
|
+
False,
|
|
672
|
+
"--llm-prompt",
|
|
673
|
+
help="Append a ready-to-use LLM prompt to the output",
|
|
674
|
+
),
|
|
675
|
+
task_help: bool = typer.Option(
|
|
676
|
+
False,
|
|
677
|
+
"--task-help",
|
|
678
|
+
help="List available tasks with descriptions and exit",
|
|
679
|
+
),
|
|
680
|
+
dry_run: bool = typer.Option(
|
|
681
|
+
False,
|
|
682
|
+
"--dry-run",
|
|
683
|
+
help="Show what would be analyzed without running it",
|
|
684
|
+
),
|
|
659
685
|
) -> None:
|
|
660
|
-
"""
|
|
661
|
-
|
|
686
|
+
"""Prepare task-aware context optimized for LLM reasoning.
|
|
687
|
+
|
|
688
|
+
\b
|
|
689
|
+
Note: PATH must be provided before the subcommand (default: '.'):
|
|
690
|
+
sourcecode . prepare-context explain
|
|
691
|
+
sourcecode . prepare-context fix-bug --path /my/project
|
|
692
|
+
sourcecode . prepare-context generate-tests --llm-prompt
|
|
693
|
+
sourcecode . prepare-context --task-help
|
|
694
|
+
"""
|
|
695
|
+
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
696
|
+
|
|
697
|
+
if task_help:
|
|
698
|
+
typer.echo("Available tasks:\n")
|
|
699
|
+
for name, spec in TASKS.items():
|
|
700
|
+
typer.echo(f" {name:<20} {spec.description}")
|
|
701
|
+
typer.echo(f" {'':20} Output: {spec.output_hint}\n")
|
|
702
|
+
raise typer.Exit()
|
|
662
703
|
|
|
663
|
-
|
|
704
|
+
if task is None:
|
|
705
|
+
typer.echo(
|
|
706
|
+
f"Error: task is required. Available: {', '.join(TASKS)}\n"
|
|
707
|
+
"Use --task-help for descriptions.",
|
|
708
|
+
err=True,
|
|
709
|
+
)
|
|
710
|
+
raise typer.Exit(code=1)
|
|
664
711
|
|
|
665
|
-
|
|
712
|
+
if task not in TASKS:
|
|
713
|
+
typer.echo(
|
|
714
|
+
f"Error: unknown task '{task}'. Available: {', '.join(TASKS)}",
|
|
715
|
+
err=True,
|
|
716
|
+
)
|
|
717
|
+
raise typer.Exit(code=1)
|
|
666
718
|
|
|
719
|
+
target = path.resolve()
|
|
667
720
|
if not target.exists() or not target.is_dir():
|
|
668
721
|
typer.echo(f"Error: '{target}' no es un directorio válido.", err=True)
|
|
669
722
|
raise typer.Exit(code=1)
|
|
670
723
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
"
|
|
676
|
-
"
|
|
677
|
-
|
|
678
|
-
"
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
724
|
+
if dry_run:
|
|
725
|
+
spec = TASKS[task]
|
|
726
|
+
typer.echo(f"task: {task}")
|
|
727
|
+
typer.echo(f"goal: {spec.goal}")
|
|
728
|
+
typer.echo(f"path: {target}")
|
|
729
|
+
typer.echo(f"analyzers: dependencies={'yes' if spec.enable_dependencies else 'no'}"
|
|
730
|
+
f", code_notes={'yes' if spec.enable_code_notes else 'no'}")
|
|
731
|
+
typer.echo(f"output: {spec.output_hint}")
|
|
732
|
+
raise typer.Exit()
|
|
733
|
+
|
|
734
|
+
from dataclasses import asdict
|
|
735
|
+
|
|
736
|
+
builder = TaskContextBuilder(target)
|
|
737
|
+
output = builder.build(task)
|
|
738
|
+
|
|
739
|
+
out: dict[str, Any] = {
|
|
740
|
+
"task": output.task,
|
|
741
|
+
"goal": output.goal,
|
|
742
|
+
"project_summary": output.project_summary,
|
|
743
|
+
"architecture_summary": output.architecture_summary,
|
|
744
|
+
"relevant_files": [asdict(f) for f in output.relevant_files],
|
|
745
|
+
"key_dependencies": output.key_dependencies,
|
|
682
746
|
}
|
|
747
|
+
if output.suspected_areas:
|
|
748
|
+
out["suspected_areas"] = output.suspected_areas
|
|
749
|
+
if output.improvement_opportunities:
|
|
750
|
+
out["improvement_opportunities"] = output.improvement_opportunities
|
|
751
|
+
if output.test_gaps:
|
|
752
|
+
out["test_gaps"] = output.test_gaps
|
|
753
|
+
if output.code_notes_summary:
|
|
754
|
+
out["code_notes_summary"] = output.code_notes_summary
|
|
755
|
+
if output.limitations:
|
|
756
|
+
out["limitations"] = output.limitations
|
|
757
|
+
if llm_prompt:
|
|
758
|
+
out["llm_prompt"] = builder.render_prompt(output)
|
|
683
759
|
|
|
684
760
|
typer.echo(json.dumps(out, indent=2, ensure_ascii=False))
|
sourcecode/prepare_context.py
CHANGED
|
@@ -1,197 +1,486 @@
|
|
|
1
|
+
"""prepare_context.py — Task-aware context optimizer for LLM reasoning."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
from dataclasses import dataclass
|
|
4
6
|
from pathlib import Path
|
|
5
|
-
from typing import Any,
|
|
7
|
+
from typing import Any, Optional
|
|
6
8
|
|
|
7
|
-
from sourcecode.semantic_analyzer import SemanticAnalyzer
|
|
8
9
|
|
|
10
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
# Prompt templates
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# =========================
|
|
14
|
+
_EXPLAIN_PROMPT = """\
|
|
15
|
+
You are an expert software engineer. Your task is to explain this project clearly.
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
## Project Summary
|
|
18
|
+
|
|
19
|
+
{project_summary}
|
|
20
|
+
|
|
21
|
+
{architecture_section}
|
|
22
|
+
|
|
23
|
+
## Key Files
|
|
24
|
+
|
|
25
|
+
{relevant_files_section}
|
|
26
|
+
|
|
27
|
+
{dependencies_section}
|
|
28
|
+
|
|
29
|
+
## Instructions
|
|
30
|
+
|
|
31
|
+
1. Summarize what this project does and who it is for.
|
|
32
|
+
2. Describe the main components and how they interact.
|
|
33
|
+
3. Identify the primary entry point and the main execution flow.
|
|
34
|
+
4. Highlight any non-obvious design decisions or constraints.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_FIX_BUG_PROMPT = """\
|
|
38
|
+
You are an expert debugger. Your task is to identify and fix a bug in this codebase.
|
|
39
|
+
|
|
40
|
+
## Project Summary
|
|
41
|
+
|
|
42
|
+
{project_summary}
|
|
43
|
+
|
|
44
|
+
## Most Relevant Files
|
|
45
|
+
|
|
46
|
+
{relevant_files_section}
|
|
47
|
+
|
|
48
|
+
{suspected_areas_section}
|
|
49
|
+
|
|
50
|
+
{code_notes_section}
|
|
51
|
+
|
|
52
|
+
## Instructions
|
|
53
|
+
|
|
54
|
+
1. Review the relevant files listed above, paying close attention to suspected areas.
|
|
55
|
+
2. Identify the root cause of the bug.
|
|
56
|
+
3. Propose a minimal, targeted fix with a concrete code patch.
|
|
57
|
+
4. Explain why the fix is correct and what side effects to watch for.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
_REFACTOR_PROMPT = """\
|
|
61
|
+
You are an expert software engineer focused on code quality. \
|
|
62
|
+
Your task is to propose refactoring improvements.
|
|
63
|
+
|
|
64
|
+
## Project Summary
|
|
65
|
+
|
|
66
|
+
{project_summary}
|
|
67
|
+
|
|
68
|
+
{architecture_section}
|
|
69
|
+
|
|
70
|
+
## Files to Refactor
|
|
71
|
+
|
|
72
|
+
{relevant_files_section}
|
|
73
|
+
|
|
74
|
+
{improvement_opportunities_section}
|
|
75
|
+
|
|
76
|
+
## Instructions
|
|
77
|
+
|
|
78
|
+
1. Identify the top 3–5 most impactful refactoring opportunities.
|
|
79
|
+
2. For each, describe the current problem, the proposed change, and the expected benefit.
|
|
80
|
+
3. Prioritize changes that reduce complexity or improve testability.
|
|
81
|
+
4. Do not suggest changes that break public APIs without noting the impact.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
_GENERATE_TESTS_PROMPT = """\
|
|
85
|
+
You are an expert in software testing. Your task is to write tests for \
|
|
86
|
+
untested or undertested areas of this codebase.
|
|
87
|
+
|
|
88
|
+
## Project Summary
|
|
89
|
+
|
|
90
|
+
{project_summary}
|
|
20
91
|
|
|
92
|
+
## Files Needing Tests
|
|
93
|
+
|
|
94
|
+
{relevant_files_section}
|
|
95
|
+
|
|
96
|
+
{test_gaps_section}
|
|
97
|
+
|
|
98
|
+
{dependencies_section}
|
|
99
|
+
|
|
100
|
+
## Instructions
|
|
101
|
+
|
|
102
|
+
1. Write unit tests for the most critical untested functions and classes.
|
|
103
|
+
2. Cover edge cases and error paths, not just the happy path.
|
|
104
|
+
3. Use the same testing framework already present in the project.
|
|
105
|
+
4. Each test must have a clear name that describes exactly what it verifies.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
# Task registry
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
21
112
|
|
|
22
113
|
@dataclass
|
|
23
|
-
class
|
|
114
|
+
class TaskSpec:
|
|
115
|
+
name: str
|
|
116
|
+
goal: str
|
|
117
|
+
description: str
|
|
118
|
+
ranking_boosts: list[str]
|
|
119
|
+
ranking_penalties: list[str]
|
|
120
|
+
enable_code_notes: bool
|
|
121
|
+
enable_dependencies: bool
|
|
122
|
+
prompt_template: str
|
|
123
|
+
output_hint: str
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
TASKS: dict[str, TaskSpec] = {
|
|
127
|
+
"explain": TaskSpec(
|
|
128
|
+
name="explain",
|
|
129
|
+
goal="Generate a comprehensive project summary for onboarding an LLM or developer.",
|
|
130
|
+
description="Analyze project structure, entry points, and key dependencies.",
|
|
131
|
+
ranking_boosts=["main", "cli", "app", "core", "index", "readme", "schema", "config"],
|
|
132
|
+
ranking_penalties=["test_", "spec_", ".min.", "__pycache__", "docs/"],
|
|
133
|
+
enable_code_notes=False,
|
|
134
|
+
enable_dependencies=True,
|
|
135
|
+
prompt_template=_EXPLAIN_PROMPT,
|
|
136
|
+
output_hint="project_summary, architecture_summary, relevant_files, key_dependencies",
|
|
137
|
+
),
|
|
138
|
+
"fix-bug": TaskSpec(
|
|
139
|
+
name="fix-bug",
|
|
140
|
+
goal="Identify the most likely files and areas where a bug may be located.",
|
|
141
|
+
description="Rank files by annotation density, surface TODOs/FIXMEs/BUGs.",
|
|
142
|
+
ranking_boosts=["handler", "service", "middleware", "router", "controller",
|
|
143
|
+
"processor", "parser", "validator"],
|
|
144
|
+
ranking_penalties=["test_", "spec_", ".min.", "__pycache__", "docs/"],
|
|
145
|
+
enable_code_notes=True,
|
|
146
|
+
enable_dependencies=False,
|
|
147
|
+
prompt_template=_FIX_BUG_PROMPT,
|
|
148
|
+
output_hint="relevant_files (ranked by risk), suspected_areas, code_notes_summary",
|
|
149
|
+
),
|
|
150
|
+
"refactor": TaskSpec(
|
|
151
|
+
name="refactor",
|
|
152
|
+
goal="Highlight structural issues and improvement opportunities across the codebase.",
|
|
153
|
+
description="Surface large files, high-annotation areas, and architectural patterns.",
|
|
154
|
+
ranking_boosts=["core", "utils", "helper", "base", "common", "shared", "lib"],
|
|
155
|
+
ranking_penalties=["test_", ".min.", "__pycache__"],
|
|
156
|
+
enable_code_notes=True,
|
|
157
|
+
enable_dependencies=False,
|
|
158
|
+
prompt_template=_REFACTOR_PROMPT,
|
|
159
|
+
output_hint="relevant_files, improvement_opportunities, architecture_summary",
|
|
160
|
+
),
|
|
161
|
+
"generate-tests": TaskSpec(
|
|
162
|
+
name="generate-tests",
|
|
163
|
+
goal="Identify untested source files and generate targeted test stubs.",
|
|
164
|
+
description="Find source files without matching test files and rank by complexity.",
|
|
165
|
+
ranking_boosts=["service", "handler", "controller", "router", "parser",
|
|
166
|
+
"validator", "processor"],
|
|
167
|
+
ranking_penalties=[".min.", "__pycache__", "docs/"],
|
|
168
|
+
enable_code_notes=False,
|
|
169
|
+
enable_dependencies=True,
|
|
170
|
+
prompt_template=_GENERATE_TESTS_PROMPT,
|
|
171
|
+
output_hint="test_gaps, relevant_files (source without tests), key_dependencies",
|
|
172
|
+
),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
# Output schema
|
|
178
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class RelevantFile:
|
|
24
182
|
path: str
|
|
25
|
-
role: str
|
|
183
|
+
role: str # entrypoint | source | test
|
|
184
|
+
score: float
|
|
185
|
+
reason: str
|
|
26
186
|
|
|
27
187
|
|
|
28
188
|
@dataclass
|
|
29
|
-
class
|
|
189
|
+
class TaskOutput:
|
|
30
190
|
task: str
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
191
|
+
goal: str
|
|
192
|
+
project_summary: Optional[str]
|
|
193
|
+
architecture_summary: Optional[str]
|
|
194
|
+
relevant_files: list[RelevantFile]
|
|
195
|
+
suspected_areas: list[str]
|
|
196
|
+
improvement_opportunities: list[str]
|
|
197
|
+
test_gaps: list[str]
|
|
198
|
+
key_dependencies: list[dict[str, Any]]
|
|
199
|
+
code_notes_summary: Optional[dict[str, Any]]
|
|
200
|
+
limitations: list[str]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
204
|
+
# Builder
|
|
205
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
37
206
|
|
|
207
|
+
_SOURCE_EXTENSIONS: frozenset[str] = frozenset({
|
|
208
|
+
".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".kt",
|
|
209
|
+
".go", ".rs", ".rb", ".php", ".cs", ".dart",
|
|
210
|
+
})
|
|
38
211
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
212
|
+
_ALL_EXTENSIONS: frozenset[str] = _SOURCE_EXTENSIONS | frozenset({
|
|
213
|
+
".md", ".toml", ".yaml", ".yml", ".json", ".xml",
|
|
214
|
+
})
|
|
42
215
|
|
|
43
|
-
|
|
216
|
+
|
|
217
|
+
class TaskContextBuilder:
|
|
44
218
|
def __init__(self, root: Path) -> None:
|
|
45
219
|
self.root = root
|
|
46
|
-
self.analyzer = SemanticAnalyzer(root)
|
|
47
220
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
221
|
+
def build(self, task_name: str) -> TaskOutput:
|
|
222
|
+
if task_name not in TASKS:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Unknown task '{task_name}'. Available: {', '.join(TASKS)}"
|
|
225
|
+
)
|
|
226
|
+
spec = TASKS[task_name]
|
|
227
|
+
|
|
228
|
+
# ── 1. Scan ────────────────────────────────────────────────────────
|
|
229
|
+
from sourcecode.scanner import FileScanner
|
|
230
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
231
|
+
|
|
232
|
+
scanner = FileScanner(self.root, max_depth=6)
|
|
233
|
+
file_tree = scanner.scan_tree()
|
|
234
|
+
manifests = scanner.find_manifests()
|
|
235
|
+
all_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
|
|
236
|
+
|
|
237
|
+
# ── 2. Detect stacks + entry points ───────────────────────────────
|
|
238
|
+
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
239
|
+
|
|
240
|
+
detector = ProjectDetector(build_default_detectors())
|
|
241
|
+
stacks, entry_points, _ = detector.detect(self.root, file_tree, manifests)
|
|
242
|
+
stacks, project_type = detector.classify_results(file_tree, stacks, entry_points)
|
|
243
|
+
|
|
244
|
+
# ── 3. Summarize ───────────────────────────────────────────────────
|
|
245
|
+
from sourcecode.schema import AnalysisMetadata, SourceMap
|
|
246
|
+
from sourcecode.summarizer import ProjectSummarizer
|
|
247
|
+
from sourcecode.architecture_summary import ArchitectureSummarizer
|
|
248
|
+
|
|
249
|
+
sm = SourceMap(
|
|
250
|
+
metadata=AnalysisMetadata(analyzed_path=str(self.root)),
|
|
251
|
+
file_tree=file_tree,
|
|
252
|
+
stacks=stacks,
|
|
253
|
+
project_type=project_type,
|
|
254
|
+
entry_points=entry_points,
|
|
255
|
+
)
|
|
256
|
+
sm.file_paths = all_paths
|
|
257
|
+
|
|
258
|
+
project_summary = ProjectSummarizer(self.root).generate(sm)
|
|
259
|
+
architecture_summary = ArchitectureSummarizer(self.root).generate(sm)
|
|
260
|
+
|
|
261
|
+
# ── 4. Dependencies ────────────────────────────────────────────────
|
|
262
|
+
key_dependencies: list[dict[str, Any]] = []
|
|
263
|
+
limitations: list[str] = []
|
|
264
|
+
|
|
265
|
+
if spec.enable_dependencies:
|
|
266
|
+
from dataclasses import asdict
|
|
267
|
+
from sourcecode.dependency_analyzer import DependencyAnalyzer
|
|
268
|
+
|
|
269
|
+
dep_records, dep_summary = DependencyAnalyzer().analyze(self.root)
|
|
270
|
+
primary_eco = stacks[0].stack if stacks else ""
|
|
271
|
+
direct = [
|
|
272
|
+
d for d in dep_records
|
|
273
|
+
if d.scope != "transitive" and d.source in {"manifest", "lockfile"}
|
|
274
|
+
]
|
|
275
|
+
direct.sort(key=lambda d: (0 if d.ecosystem == primary_eco else 1, d.name.lower()))
|
|
276
|
+
key_dependencies = [asdict(d) for d in direct[:15]]
|
|
277
|
+
limitations.extend(dep_summary.limitations)
|
|
278
|
+
|
|
279
|
+
# ── 5. Code notes ──────────────────────────────────────────────────
|
|
280
|
+
code_notes_summary: Optional[dict[str, Any]] = None
|
|
281
|
+
suspected_areas: list[str] = []
|
|
282
|
+
improvement_opportunities: list[str] = []
|
|
283
|
+
|
|
284
|
+
if spec.enable_code_notes:
|
|
285
|
+
from dataclasses import asdict
|
|
286
|
+
from sourcecode.code_notes_analyzer import CodeNotesAnalyzer
|
|
287
|
+
|
|
288
|
+
cn_notes, _cn_adrs, cn_summary = CodeNotesAnalyzer().analyze(self.root)
|
|
289
|
+
code_notes_summary = asdict(cn_summary)
|
|
290
|
+
|
|
291
|
+
if task_name == "fix-bug":
|
|
292
|
+
bug_kinds = {"FIXME", "BUG", "HACK", "XXX"}
|
|
293
|
+
counts: dict[str, int] = {}
|
|
294
|
+
for note in cn_notes:
|
|
295
|
+
if note.kind in bug_kinds:
|
|
296
|
+
counts[note.path] = counts.get(note.path, 0) + 1
|
|
297
|
+
suspected_areas = [
|
|
298
|
+
f"{p} ({n} annotation{'s' if n > 1 else ''})"
|
|
299
|
+
for p, n in sorted(counts.items(), key=lambda x: -x[1])[:8]
|
|
300
|
+
]
|
|
301
|
+
|
|
302
|
+
elif task_name == "refactor":
|
|
303
|
+
ref_kinds = {"TODO", "DEPRECATED", "OPTIMIZE", "HACK"}
|
|
304
|
+
counts2: dict[str, int] = {}
|
|
305
|
+
for note in cn_notes:
|
|
306
|
+
if note.kind in ref_kinds:
|
|
307
|
+
counts2[note.path] = counts2.get(note.path, 0) + 1
|
|
308
|
+
improvement_opportunities = [
|
|
309
|
+
f"{p}: {n} refactoring annotation{'s' if n > 1 else ''}"
|
|
310
|
+
for p, n in sorted(counts2.items(), key=lambda x: -x[1])[:8]
|
|
311
|
+
]
|
|
312
|
+
|
|
313
|
+
# ── 6. Rank files ──────────────────────────────────────────────────
|
|
314
|
+
entry_set = {ep.path for ep in entry_points}
|
|
315
|
+
test_set = {p for p in all_paths if self._is_test(p)}
|
|
316
|
+
source_set = {p for p in all_paths if not self._is_test(p) and self._is_source(p)}
|
|
317
|
+
|
|
318
|
+
relevant_files = self._rank_files(
|
|
319
|
+
task_name, spec, all_paths, entry_set, test_set
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# ── 7. Test gaps (generate-tests only) ────────────────────────────
|
|
323
|
+
test_gaps: list[str] = []
|
|
324
|
+
if task_name == "generate-tests":
|
|
325
|
+
test_stems = {
|
|
326
|
+
Path(p).stem.removeprefix("test_").removesuffix("_test")
|
|
327
|
+
for p in test_set
|
|
328
|
+
}
|
|
329
|
+
untested = [
|
|
330
|
+
p for p in source_set
|
|
331
|
+
if Path(p).stem not in test_stems
|
|
332
|
+
and not any(pen in p for pen in spec.ranking_penalties)
|
|
333
|
+
]
|
|
334
|
+
untested.sort(key=lambda p: (len(p.split("/")), p))
|
|
335
|
+
test_gaps = untested[:15]
|
|
336
|
+
|
|
337
|
+
return TaskOutput(
|
|
338
|
+
task=task_name,
|
|
339
|
+
goal=spec.goal,
|
|
340
|
+
project_summary=project_summary,
|
|
341
|
+
architecture_summary=architecture_summary,
|
|
342
|
+
relevant_files=relevant_files,
|
|
343
|
+
suspected_areas=suspected_areas,
|
|
344
|
+
improvement_opportunities=improvement_opportunities,
|
|
345
|
+
test_gaps=test_gaps,
|
|
346
|
+
key_dependencies=key_dependencies,
|
|
347
|
+
code_notes_summary=code_notes_summary,
|
|
348
|
+
limitations=limitations,
|
|
349
|
+
)
|
|
51
350
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
Entry point principal.
|
|
55
|
-
"""
|
|
56
|
-
semantic = self.analyzer.analyze()
|
|
351
|
+
def render_prompt(self, output: TaskOutput) -> str:
|
|
352
|
+
spec = TASKS[output.task]
|
|
57
353
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
snippets = self._extract_snippets(relevant_files)
|
|
61
|
-
tests = self._find_related_tests(relevant_files)
|
|
62
|
-
call_flow = self._build_call_flow(entry_points)
|
|
354
|
+
def _section(title: str, body: str) -> str:
|
|
355
|
+
return f"## {title}\n\n{body}" if body.strip() else ""
|
|
63
356
|
|
|
64
|
-
|
|
357
|
+
project_summary = output.project_summary or "No project summary available."
|
|
65
358
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
entry_points=entry_points,
|
|
69
|
-
relevant_files=relevant_files,
|
|
70
|
-
call_flow=call_flow,
|
|
71
|
-
snippets=snippets,
|
|
72
|
-
tests=tests,
|
|
73
|
-
notes=notes,
|
|
359
|
+
architecture_section = _section(
|
|
360
|
+
"Architecture", output.architecture_summary or ""
|
|
74
361
|
)
|
|
75
362
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
363
|
+
relevant_files_section = "\n".join(
|
|
364
|
+
f"- `{f.path}` [{f.role}] — {f.reason}"
|
|
365
|
+
for f in output.relevant_files
|
|
366
|
+
) or "No relevant files identified."
|
|
367
|
+
|
|
368
|
+
deps_lines = [
|
|
369
|
+
f"- {d['name']} "
|
|
370
|
+
f"{d.get('declared_version') or d.get('resolved_version') or '(unknown)'}"
|
|
371
|
+
for d in output.key_dependencies[:10]
|
|
372
|
+
]
|
|
373
|
+
dependencies_section = _section("Key Dependencies", "\n".join(deps_lines))
|
|
374
|
+
|
|
375
|
+
suspected_areas_section = _section(
|
|
376
|
+
"Suspected Problem Areas",
|
|
377
|
+
"\n".join(f"- {a}" for a in output.suspected_areas),
|
|
378
|
+
)
|
|
79
379
|
|
|
80
|
-
|
|
81
|
-
""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
380
|
+
code_notes_section = ""
|
|
381
|
+
if output.code_notes_summary and output.code_notes_summary.get("total", 0) > 0:
|
|
382
|
+
by_kind = output.code_notes_summary.get("by_kind", {})
|
|
383
|
+
kinds_str = ", ".join(f"{k}: {v}" for k, v in by_kind.items() if v > 0)
|
|
384
|
+
top_files = ", ".join(output.code_notes_summary.get("top_files", [])[:5])
|
|
385
|
+
code_notes_section = _section(
|
|
386
|
+
"Code Annotations",
|
|
387
|
+
f"Total: {output.code_notes_summary['total']} ({kinds_str})\n"
|
|
388
|
+
f"Most annotated: {top_files}",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
improvement_opportunities_section = _section(
|
|
392
|
+
"Improvement Opportunities",
|
|
393
|
+
"\n".join(f"- {o}" for o in output.improvement_opportunities),
|
|
394
|
+
)
|
|
85
395
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
396
|
+
test_gaps_section = _section(
|
|
397
|
+
"Untested Files",
|
|
398
|
+
"\n".join(f"- `{p}`" for p in output.test_gaps),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return spec.prompt_template.format(
|
|
402
|
+
project_summary=project_summary,
|
|
403
|
+
architecture_section=architecture_section,
|
|
404
|
+
relevant_files_section=relevant_files_section,
|
|
405
|
+
dependencies_section=dependencies_section,
|
|
406
|
+
suspected_areas_section=suspected_areas_section,
|
|
407
|
+
code_notes_section=code_notes_section,
|
|
408
|
+
improvement_opportunities_section=improvement_opportunities_section,
|
|
409
|
+
test_gaps_section=test_gaps_section,
|
|
410
|
+
).strip()
|
|
93
411
|
|
|
94
|
-
|
|
412
|
+
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
95
413
|
|
|
96
|
-
def
|
|
414
|
+
def _rank_files(
|
|
97
415
|
self,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
keywords = task.lower().split()
|
|
112
|
-
|
|
113
|
-
for path in self.analyzer.scan.file_paths: # fallback simple
|
|
114
|
-
if any(k in path.lower() for k in keywords):
|
|
115
|
-
files.append(ContextFile(path=path, role="matched"))
|
|
116
|
-
|
|
117
|
-
# deduplicar
|
|
118
|
-
seen = set()
|
|
119
|
-
unique = []
|
|
120
|
-
for f in files:
|
|
121
|
-
if f.path not in seen:
|
|
122
|
-
seen.add(f.path)
|
|
123
|
-
unique.append(f)
|
|
124
|
-
|
|
125
|
-
return unique[:8]
|
|
126
|
-
|
|
127
|
-
def _extract_snippets(self, files: List[ContextFile]) -> List[ContextSnippet]:
|
|
128
|
-
"""
|
|
129
|
-
Extrae primeras ~30 líneas como snippet simple.
|
|
130
|
-
"""
|
|
131
|
-
snippets: List[ContextSnippet] = []
|
|
132
|
-
|
|
133
|
-
for f in files:
|
|
134
|
-
abs_path = self.root / f.path
|
|
135
|
-
if not abs_path.exists():
|
|
416
|
+
task_name: str,
|
|
417
|
+
spec: TaskSpec,
|
|
418
|
+
all_paths: list[str],
|
|
419
|
+
entry_set: set[str],
|
|
420
|
+
test_set: set[str],
|
|
421
|
+
) -> list[RelevantFile]:
|
|
422
|
+
scored: list[tuple[float, RelevantFile]] = []
|
|
423
|
+
|
|
424
|
+
for path in all_paths:
|
|
425
|
+
if Path(path).suffix.lower() not in _ALL_EXTENSIONS:
|
|
426
|
+
continue
|
|
427
|
+
if any(pen in path for pen in spec.ranking_penalties):
|
|
136
428
|
continue
|
|
137
429
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
except Exception:
|
|
430
|
+
is_test = path in test_set
|
|
431
|
+
if is_test and task_name != "generate-tests":
|
|
141
432
|
continue
|
|
142
433
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return notes
|
|
434
|
+
score = 0.0
|
|
435
|
+
reasons: list[str] = []
|
|
436
|
+
|
|
437
|
+
if path in entry_set:
|
|
438
|
+
score += 3.0
|
|
439
|
+
reasons.append("entry point")
|
|
440
|
+
|
|
441
|
+
path_lower = path.lower()
|
|
442
|
+
for keyword in spec.ranking_boosts:
|
|
443
|
+
if keyword in path_lower:
|
|
444
|
+
score += 1.5
|
|
445
|
+
reasons.append(f"matches '{keyword}'")
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
if is_test:
|
|
449
|
+
score += 2.0
|
|
450
|
+
reasons.append("existing test")
|
|
451
|
+
elif self._is_source(path):
|
|
452
|
+
score += 0.5
|
|
453
|
+
if not reasons:
|
|
454
|
+
reasons.append("source file")
|
|
455
|
+
|
|
456
|
+
if score <= 0:
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
role = (
|
|
460
|
+
"entrypoint" if path in entry_set
|
|
461
|
+
else ("test" if is_test else "source")
|
|
462
|
+
)
|
|
463
|
+
scored.append((score, RelevantFile(
|
|
464
|
+
path=path,
|
|
465
|
+
role=role,
|
|
466
|
+
score=round(score, 1),
|
|
467
|
+
reason=", ".join(reasons) if reasons else "source file",
|
|
468
|
+
)))
|
|
469
|
+
|
|
470
|
+
scored.sort(key=lambda x: -x[0])
|
|
471
|
+
return [f for _, f in scored[:15]]
|
|
472
|
+
|
|
473
|
+
def _is_test(self, path: str) -> bool:
|
|
474
|
+
name = Path(path).name.lower()
|
|
475
|
+
return (
|
|
476
|
+
name.startswith("test_")
|
|
477
|
+
or name.endswith("_test.py")
|
|
478
|
+
or name.endswith(".test.ts")
|
|
479
|
+
or name.endswith(".spec.ts")
|
|
480
|
+
or "/tests/" in path
|
|
481
|
+
or "/test/" in path
|
|
482
|
+
or "/spec/" in path
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def _is_source(self, path: str) -> bool:
|
|
486
|
+
return Path(path).suffix.lower() in _SOURCE_EXTENSIONS
|
sourcecode/semantic_analyzer.py
CHANGED
|
@@ -70,12 +70,14 @@ class SemanticAnalyzer:
|
|
|
70
70
|
|
|
71
71
|
def __init__(
|
|
72
72
|
self,
|
|
73
|
+
root: Optional[Path] = None,
|
|
73
74
|
*,
|
|
74
75
|
max_files: int = _MAX_FILES,
|
|
75
76
|
max_file_size: int = _MAX_FILE_SIZE,
|
|
76
77
|
max_calls: int = _MAX_CALLS,
|
|
77
78
|
max_symbols: int = _MAX_SYMBOLS,
|
|
78
79
|
) -> None:
|
|
80
|
+
self.root = root
|
|
79
81
|
self.max_files = max_files
|
|
80
82
|
self.max_file_size = max_file_size
|
|
81
83
|
self.max_calls = max_calls
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.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
|
|
@@ -636,6 +636,75 @@ Combine flags for a comprehensive project handoff:
|
|
|
636
636
|
sourcecode --compact --env-map --code-notes --git-context .
|
|
637
637
|
```
|
|
638
638
|
|
|
639
|
+
## `prepare-context` — Task-aware context for LLMs
|
|
640
|
+
|
|
641
|
+
`prepare-context` is a subcommand that builds a focused, task-specific context optimized for LLM reasoning. Instead of a full project dump, it returns only the data an LLM needs for a specific goal.
|
|
642
|
+
|
|
643
|
+
> **Note:** Because `sourcecode` has a positional `PATH` argument, you must provide it explicitly before the subcommand:
|
|
644
|
+
> ```bash
|
|
645
|
+
> sourcecode . prepare-context <task> # current directory
|
|
646
|
+
> sourcecode /my/project prepare-context <task>
|
|
647
|
+
> ```
|
|
648
|
+
|
|
649
|
+
### Available tasks
|
|
650
|
+
|
|
651
|
+
| Task | Goal | Output |
|
|
652
|
+
|------|------|--------|
|
|
653
|
+
| `explain` | Onboard an LLM to the project | `project_summary`, `architecture_summary`, `relevant_files`, `key_dependencies` |
|
|
654
|
+
| `fix-bug` | Identify likely bug locations | `relevant_files` (ranked by risk), `suspected_areas`, `code_notes_summary` |
|
|
655
|
+
| `refactor` | Surface improvement opportunities | `relevant_files`, `improvement_opportunities`, `architecture_summary` |
|
|
656
|
+
| `generate-tests` | Find untested areas | `test_gaps`, `relevant_files` (source without tests), `key_dependencies` |
|
|
657
|
+
|
|
658
|
+
### Usage
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
# List tasks with descriptions
|
|
662
|
+
sourcecode . prepare-context --task-help
|
|
663
|
+
|
|
664
|
+
# Explain the project
|
|
665
|
+
sourcecode . prepare-context explain
|
|
666
|
+
|
|
667
|
+
# Find bug areas, with a ready-to-paste LLM prompt
|
|
668
|
+
sourcecode . prepare-context fix-bug --llm-prompt
|
|
669
|
+
|
|
670
|
+
# Find untested files in a specific project
|
|
671
|
+
sourcecode . prepare-context generate-tests --path /my/project
|
|
672
|
+
|
|
673
|
+
# Preview what will be analyzed (no analysis run)
|
|
674
|
+
sourcecode . prepare-context refactor --dry-run
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Output format
|
|
678
|
+
|
|
679
|
+
```json
|
|
680
|
+
{
|
|
681
|
+
"task": "fix-bug",
|
|
682
|
+
"goal": "Identify the most likely files and areas where a bug may be located.",
|
|
683
|
+
"project_summary": "CLI en Python (Typer). Entry points: src/cli.py. 4 dependencias.",
|
|
684
|
+
"architecture_summary": null,
|
|
685
|
+
"relevant_files": [
|
|
686
|
+
{ "path": "src/handler.py", "role": "source", "score": 2.0, "reason": "matches 'handler'" },
|
|
687
|
+
{ "path": "src/cli.py", "role": "entrypoint", "score": 3.0, "reason": "entry point" }
|
|
688
|
+
],
|
|
689
|
+
"suspected_areas": ["src/handler.py (2 annotations)", "src/parser.py (1 annotation)"],
|
|
690
|
+
"code_notes_summary": { "total": 5, "by_kind": { "FIXME": 3, "BUG": 2 }, "top_files": ["src/handler.py"] }
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### `--llm-prompt`
|
|
695
|
+
|
|
696
|
+
Add `--llm-prompt` to include a ready-to-use prompt that you can paste directly into any LLM:
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
sourcecode . prepare-context explain --llm-prompt | jq -r '.llm_prompt'
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
The prompt is task-specific and includes the project context, relevant files, and concrete instructions for the LLM.
|
|
703
|
+
|
|
704
|
+
### Task auto-selection with `--agent`
|
|
705
|
+
|
|
706
|
+
For zero-config usage, `--agent` on the main command automatically selects the right flags for the project type. For task-aware context, use `prepare-context` explicitly with the task that matches your goal.
|
|
707
|
+
|
|
639
708
|
## Development
|
|
640
709
|
|
|
641
710
|
Editable install with development dependencies:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=Wl0aL5XpGhZsWRJOan0FtriAIMa21IHgxlCXLjVcxOI,100
|
|
2
2
|
sourcecode/architecture_analyzer.py,sha256=zxgqeqn2FhIUDn06X-whjGKiXbrHFkRlzZcr2zKd0Gw,16874
|
|
3
3
|
sourcecode/architecture_summary.py,sha256=r9IciS9z-7h8xpiME7DKBLhLV36xzXtSzbZ1cRDWklo,11709
|
|
4
4
|
sourcecode/classifier.py,sha256=fcp2wIq198wq3Rsh_bDmZIK_W5UI6FEL5oqm1RrQfpA,6846
|
|
5
|
-
sourcecode/cli.py,sha256=
|
|
5
|
+
sourcecode/cli.py,sha256=1ozxlC43rd2boOZbARb3l2csVpTFiBw8DRDzCtLywTI,29065
|
|
6
6
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
7
7
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
8
8
|
sourcecode/dependency_analyzer.py,sha256=2TYBIpenhfWf_TVBZZfq3T0SHFv8xCKFIyn1XnFHG6s,48146
|
|
@@ -11,11 +11,11 @@ sourcecode/env_analyzer.py,sha256=JZxBOuIxnM0xw0IaJFL6LiPJBErL848L3XoTOHGBqZI,13
|
|
|
11
11
|
sourcecode/git_analyzer.py,sha256=S9PGt8RDasBQYQUsZh9-H_KWVlPvzwR4jgM7TKN9ZCk,7643
|
|
12
12
|
sourcecode/graph_analyzer.py,sha256=cxl7Xb88aGxIVOCsatZJAE1CtgXGzIbJfqytV-9kkFs,45172
|
|
13
13
|
sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
|
|
14
|
-
sourcecode/prepare_context.py,sha256=
|
|
14
|
+
sourcecode/prepare_context.py,sha256=yPRohYZ3wvX156GQSo1vYuoCDBIG3uy3G-3DdNbGHKU,19271
|
|
15
15
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
16
16
|
sourcecode/scanner.py,sha256=Ga4jDRAOhkcwlRmlE2ko6cE39qKy0aI4iE1lGBWmAlk,6410
|
|
17
17
|
sourcecode/schema.py,sha256=dnNdnQxJtBv0PRVWi6-xnpr16dj6IsUGbso9PeeEolA,15743
|
|
18
|
-
sourcecode/semantic_analyzer.py,sha256=
|
|
18
|
+
sourcecode/semantic_analyzer.py,sha256=asQfJf-EhzYaOTA-iMuZsrVXtbW7SV2WEKCxgsxa88Y,79413
|
|
19
19
|
sourcecode/serializer.py,sha256=ck0gK3PV-rY5UrAvykKh8x56Lb1135J23xzbcEsvUW8,13772
|
|
20
20
|
sourcecode/summarizer.py,sha256=2Yz5xJ1bhtJLlrUpKcd74wijypRRo9GJmGT8u971EW0,11572
|
|
21
21
|
sourcecode/tree_utils.py,sha256=xla45Whq24j6U9kEgTztJXkMpmOEDieAulKxP3nl89s,935
|
|
@@ -39,7 +39,7 @@ sourcecode/detectors/rust.py,sha256=V23NO6Y8NsrhrUlGC2feUEcNK63hf3gc1y3jI4tes1Q,
|
|
|
39
39
|
sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
|
|
40
40
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
41
41
|
sourcecode/detectors/tooling.py,sha256=hIvop80No22pqyGVJ32NKliSdjkHRePQkIRroqG01bY,1875
|
|
42
|
-
sourcecode-0.
|
|
43
|
-
sourcecode-0.
|
|
44
|
-
sourcecode-0.
|
|
45
|
-
sourcecode-0.
|
|
42
|
+
sourcecode-0.21.0.dist-info/METADATA,sha256=9gqiONC_cEbjYwoYox60Dx-2m8qzP3KwkNRqU8kis88,30326
|
|
43
|
+
sourcecode-0.21.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
44
|
+
sourcecode-0.21.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
45
|
+
sourcecode-0.21.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|