devsper 2.1.6__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.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Extract the public API surface: non-_ prefixed functions and classes with signatures."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiSurfaceExtractorTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Extract public API: top-level functions and classes that do not start with underscore.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "api_surface_extractor"
|
|
16
|
+
description = "Extract public API surface (non-_ names) with argument lists."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Path to file or directory"},
|
|
21
|
+
"max_symbols": {"type": "integer", "description": "Max symbols (default 80)"},
|
|
22
|
+
},
|
|
23
|
+
"required": ["path"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _arg_list(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
27
|
+
args = [a.arg for a in node.args.args if a.arg != "self"]
|
|
28
|
+
if node.args.vararg:
|
|
29
|
+
args.append("*" + node.args.vararg.arg)
|
|
30
|
+
if node.args.kwarg:
|
|
31
|
+
args.append("**" + node.args.kwarg.arg)
|
|
32
|
+
return ", ".join(args)
|
|
33
|
+
|
|
34
|
+
def run(self, **kwargs) -> str:
|
|
35
|
+
path = kwargs.get("path")
|
|
36
|
+
max_symbols = kwargs.get("max_symbols", 80)
|
|
37
|
+
if not path or not isinstance(path, str):
|
|
38
|
+
return "Error: path must be a non-empty string"
|
|
39
|
+
if not isinstance(max_symbols, int) or max_symbols < 1:
|
|
40
|
+
max_symbols = 80
|
|
41
|
+
p = Path(path).resolve()
|
|
42
|
+
if not p.exists():
|
|
43
|
+
return f"Error: path not found: {path}"
|
|
44
|
+
files = [p] if p.is_file() and p.suffix == ".py" else list(p.rglob("*.py")) if p.is_dir() else []
|
|
45
|
+
symbols = []
|
|
46
|
+
for f in files:
|
|
47
|
+
if not f.is_file():
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
tree = ast.parse(f.read_text(encoding="utf-8", errors="replace"))
|
|
51
|
+
rel = f.relative_to(p if p.is_dir() else f.parent)
|
|
52
|
+
for node in ast.iter_child_nodes(tree):
|
|
53
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
54
|
+
if node.name.startswith("_"):
|
|
55
|
+
continue
|
|
56
|
+
kind = "class" if isinstance(node, ast.ClassDef) else "def"
|
|
57
|
+
if kind == "def":
|
|
58
|
+
sig = f"{node.name}({self._arg_list(node)})" # type: ignore[arg-type]
|
|
59
|
+
else:
|
|
60
|
+
sig = node.name
|
|
61
|
+
symbols.append({"file": str(rel), "kind": kind, "signature": sig})
|
|
62
|
+
if len(symbols) >= max_symbols:
|
|
63
|
+
break
|
|
64
|
+
except (SyntaxError, OSError):
|
|
65
|
+
continue
|
|
66
|
+
if len(symbols) >= max_symbols:
|
|
67
|
+
break
|
|
68
|
+
import json
|
|
69
|
+
|
|
70
|
+
return json.dumps({"api": symbols}, indent=2)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
register(ApiSurfaceExtractorTool())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Analyze codebase architecture: package layout, entry points, and high-level structure."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from devsper.tools.base import Tool
|
|
6
|
+
from devsper.tools.registry import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ArchitectureAnalyzerTool(Tool):
|
|
10
|
+
"""
|
|
11
|
+
Report high-level architecture: top-level packages, __main__ and main-like files, and layout.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
name = "architecture_analyzer"
|
|
15
|
+
description = "Analyze codebase architecture: packages, entry points, layout."
|
|
16
|
+
input_schema = {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"path": {"type": "string", "description": "Root path"},
|
|
20
|
+
"max_depth": {"type": "integer", "description": "Max depth for layout (default 3)"},
|
|
21
|
+
},
|
|
22
|
+
"required": ["path"],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def run(self, **kwargs) -> str:
|
|
26
|
+
path = kwargs.get("path")
|
|
27
|
+
max_depth = kwargs.get("max_depth", 3)
|
|
28
|
+
if not path or not isinstance(path, str):
|
|
29
|
+
return "Error: path must be a non-empty string"
|
|
30
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
31
|
+
max_depth = 3
|
|
32
|
+
root = Path(path).resolve()
|
|
33
|
+
if not root.exists() or not root.is_dir():
|
|
34
|
+
return f"Error: path must be an existing directory: {path}"
|
|
35
|
+
top_dirs = []
|
|
36
|
+
for d in root.iterdir():
|
|
37
|
+
if d.is_dir() and not d.name.startswith(".") and d.name != "__pycache__":
|
|
38
|
+
top_dirs.append(d.name)
|
|
39
|
+
main_files = []
|
|
40
|
+
for p in root.rglob("*.py"):
|
|
41
|
+
if not p.is_file():
|
|
42
|
+
continue
|
|
43
|
+
name = p.name.lower()
|
|
44
|
+
if name in ("__main__.py", "main.py", "run.py", "app.py"):
|
|
45
|
+
main_files.append(str(p.relative_to(root)).replace("\\", "/"))
|
|
46
|
+
layout = []
|
|
47
|
+
for d in sorted(top_dirs)[:20]:
|
|
48
|
+
sub = root / d
|
|
49
|
+
if sub.is_dir():
|
|
50
|
+
py_count = sum(1 for _ in sub.rglob("*.py"))
|
|
51
|
+
layout.append(f" {d}/ ({py_count} .py files)")
|
|
52
|
+
lines = [
|
|
53
|
+
"Architecture summary",
|
|
54
|
+
"=" * 40,
|
|
55
|
+
"Top-level packages: " + ", ".join(sorted(top_dirs)[:15]),
|
|
56
|
+
"",
|
|
57
|
+
"Entry-point-like files: " + ", ".join(main_files[:10]) or "none found",
|
|
58
|
+
"",
|
|
59
|
+
"Layout (top-level):",
|
|
60
|
+
"\n".join(layout),
|
|
61
|
+
]
|
|
62
|
+
return "\n".join(lines)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
register(ArchitectureAnalyzerTool())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Index a codebase: list modules, files, and top-level symbols (functions/classes)."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CodebaseIndexerTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Build a simple index of a Python codebase: files, modules, and top-level function/class names.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "codebase_indexer"
|
|
16
|
+
description = "Index a Python codebase: list files and top-level functions/classes."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Root path to index"},
|
|
21
|
+
"max_depth": {"type": "integer", "description": "Max directory depth (default 5)"},
|
|
22
|
+
"extensions": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": {"type": "string"},
|
|
25
|
+
"description": "File extensions to include (default ['.py'])",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
"required": ["path"],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def _index_file(self, p: Path) -> list[str]:
|
|
32
|
+
symbols = []
|
|
33
|
+
try:
|
|
34
|
+
tree = ast.parse(p.read_text(encoding="utf-8", errors="replace"))
|
|
35
|
+
for node in ast.walk(tree):
|
|
36
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
37
|
+
if node.col_offset == 0:
|
|
38
|
+
kind = "class" if isinstance(node, ast.ClassDef) else "function"
|
|
39
|
+
symbols.append(f"{kind}:{node.name}")
|
|
40
|
+
except (SyntaxError, OSError):
|
|
41
|
+
pass
|
|
42
|
+
return symbols
|
|
43
|
+
|
|
44
|
+
def run(self, **kwargs) -> str:
|
|
45
|
+
path = kwargs.get("path")
|
|
46
|
+
max_depth = kwargs.get("max_depth", 5)
|
|
47
|
+
extensions = kwargs.get("extensions") or [".py"]
|
|
48
|
+
if not path or not isinstance(path, str):
|
|
49
|
+
return "Error: path must be a non-empty string"
|
|
50
|
+
if not isinstance(max_depth, int) or max_depth < 1:
|
|
51
|
+
max_depth = 5
|
|
52
|
+
root = Path(path).resolve()
|
|
53
|
+
if not root.exists() or not root.is_dir():
|
|
54
|
+
return f"Error: path must be an existing directory: {path}"
|
|
55
|
+
index = []
|
|
56
|
+
for depth, _ in enumerate(root.rglob("*")):
|
|
57
|
+
if depth > 1000:
|
|
58
|
+
break
|
|
59
|
+
for p in root.rglob("*"):
|
|
60
|
+
if p.is_file() and p.suffix.lower() in extensions:
|
|
61
|
+
rel = p.relative_to(root)
|
|
62
|
+
if len(rel.parts) > max_depth:
|
|
63
|
+
continue
|
|
64
|
+
symbols = self._index_file(p)
|
|
65
|
+
index.append({"file": str(rel), "symbols": symbols})
|
|
66
|
+
import json
|
|
67
|
+
|
|
68
|
+
return json.dumps({"root": str(root), "index": index[:200]}, indent=2)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
register(CodebaseIndexerTool())
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Build a dependency graph from Python files: module -> imported modules."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DependencyGraphBuilderTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Build a module-level dependency graph: for each .py file, list its imports (internal or external).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "dependency_graph_builder"
|
|
16
|
+
description = "Build a dependency graph from Python files (module -> imports)."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Root path of the codebase"},
|
|
21
|
+
"max_files": {"type": "integer", "description": "Max files to scan (default 150)"},
|
|
22
|
+
},
|
|
23
|
+
"required": ["path"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _imports(self, p: Path) -> list[str]:
|
|
27
|
+
imports = []
|
|
28
|
+
try:
|
|
29
|
+
tree = ast.parse(p.read_text(encoding="utf-8", errors="replace"))
|
|
30
|
+
for node in ast.walk(tree):
|
|
31
|
+
if isinstance(node, ast.Import):
|
|
32
|
+
for alias in node.names:
|
|
33
|
+
imports.append(alias.name.split(".")[0])
|
|
34
|
+
elif isinstance(node, ast.ImportFrom):
|
|
35
|
+
if node.module:
|
|
36
|
+
imports.append(node.module.split(".")[0])
|
|
37
|
+
except (SyntaxError, OSError):
|
|
38
|
+
pass
|
|
39
|
+
return list(dict.fromkeys(imports))
|
|
40
|
+
|
|
41
|
+
def run(self, **kwargs) -> str:
|
|
42
|
+
path = kwargs.get("path")
|
|
43
|
+
max_files = kwargs.get("max_files", 150)
|
|
44
|
+
if not path or not isinstance(path, str):
|
|
45
|
+
return "Error: path must be a non-empty string"
|
|
46
|
+
if not isinstance(max_files, int) or max_files < 1:
|
|
47
|
+
max_files = 150
|
|
48
|
+
root = Path(path).resolve()
|
|
49
|
+
if not root.exists() or not root.is_dir():
|
|
50
|
+
return f"Error: path must be an existing directory: {path}"
|
|
51
|
+
edges = []
|
|
52
|
+
count = 0
|
|
53
|
+
for p in root.rglob("*.py"):
|
|
54
|
+
if count >= max_files:
|
|
55
|
+
break
|
|
56
|
+
if not p.is_file():
|
|
57
|
+
continue
|
|
58
|
+
count += 1
|
|
59
|
+
mod = str(p.relative_to(root)).replace("\\", "/").replace(".py", "").replace("/", ".")
|
|
60
|
+
for imp in self._imports(p):
|
|
61
|
+
edges.append({"from": mod, "to": imp})
|
|
62
|
+
import json
|
|
63
|
+
|
|
64
|
+
return json.dumps({"nodes": "inferred from edges", "edges": edges[:300]}, indent=2)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
register(DependencyGraphBuilderTool())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Detect simple design patterns in Python code: singleton-like, context manager, etc."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DesignPatternDetectorTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Detect heuristic design patterns: class with __enter__/__exit__, base class usage, etc.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "design_pattern_detector"
|
|
16
|
+
description = "Detect simple design patterns in Python code (context manager, inheritance)."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Path to file or directory"},
|
|
21
|
+
"max_results": {"type": "integer", "description": "Max results (default 30)"},
|
|
22
|
+
},
|
|
23
|
+
"required": ["path"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def run(self, **kwargs) -> str:
|
|
27
|
+
path = kwargs.get("path")
|
|
28
|
+
max_results = kwargs.get("max_results", 30)
|
|
29
|
+
if not path or not isinstance(path, str):
|
|
30
|
+
return "Error: path must be a non-empty string"
|
|
31
|
+
if not isinstance(max_results, int) or max_results < 1:
|
|
32
|
+
max_results = 30
|
|
33
|
+
p = Path(path).resolve()
|
|
34
|
+
if not p.exists():
|
|
35
|
+
return f"Error: path not found: {path}"
|
|
36
|
+
files = [p] if p.is_file() and p.suffix == ".py" else list(p.rglob("*.py")) if p.is_dir() else []
|
|
37
|
+
patterns = []
|
|
38
|
+
for f in files:
|
|
39
|
+
if not f.is_file():
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
tree = ast.parse(f.read_text(encoding="utf-8", errors="replace"))
|
|
43
|
+
rel = str(f.relative_to(p if p.is_dir() else f.parent)).replace("\\", "/")
|
|
44
|
+
for node in ast.walk(tree):
|
|
45
|
+
if isinstance(node, ast.ClassDef):
|
|
46
|
+
names = {n.name for n in ast.iter_child_nodes(node) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))}
|
|
47
|
+
if "__enter__" in names and "__exit__" in names:
|
|
48
|
+
patterns.append({"file": rel, "pattern": "context_manager", "class": node.name})
|
|
49
|
+
if node.bases:
|
|
50
|
+
patterns.append({"file": rel, "pattern": "inheritance", "class": node.name, "bases": [ast.unparse(b) for b in node.bases[:3]]})
|
|
51
|
+
if len(patterns) >= max_results:
|
|
52
|
+
break
|
|
53
|
+
except (SyntaxError, OSError):
|
|
54
|
+
continue
|
|
55
|
+
if len(patterns) >= max_results:
|
|
56
|
+
break
|
|
57
|
+
import json
|
|
58
|
+
|
|
59
|
+
return json.dumps({"patterns": patterns}, indent=2)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
register(DesignPatternDetectorTool())
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Detect large functions by line count and list them with locations."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LargeFunctionDetectorTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
List functions that exceed a line-count threshold (default 50 lines).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "large_function_detector"
|
|
16
|
+
description = "Detect large functions by line count in Python files."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Path to file or directory"},
|
|
21
|
+
"min_lines": {"type": "integer", "description": "Minimum lines to flag (default 50)"},
|
|
22
|
+
"max_results": {"type": "integer", "description": "Max results (default 25)"},
|
|
23
|
+
},
|
|
24
|
+
"required": ["path"],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def run(self, **kwargs) -> str:
|
|
28
|
+
path = kwargs.get("path")
|
|
29
|
+
min_lines = kwargs.get("min_lines", 50)
|
|
30
|
+
max_results = kwargs.get("max_results", 25)
|
|
31
|
+
if not path or not isinstance(path, str):
|
|
32
|
+
return "Error: path must be a non-empty string"
|
|
33
|
+
if not isinstance(min_lines, int) or min_lines < 1:
|
|
34
|
+
min_lines = 50
|
|
35
|
+
if not isinstance(max_results, int) or max_results < 1:
|
|
36
|
+
max_results = 25
|
|
37
|
+
p = Path(path).resolve()
|
|
38
|
+
if not p.exists():
|
|
39
|
+
return f"Error: path not found: {path}"
|
|
40
|
+
files = [p] if p.is_file() and p.suffix == ".py" else list(p.rglob("*.py")) if p.is_dir() else []
|
|
41
|
+
large = []
|
|
42
|
+
for f in files:
|
|
43
|
+
if not f.is_file():
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
47
|
+
tree = ast.parse(text)
|
|
48
|
+
rel = str(f.relative_to(p if p.is_dir() else f.parent)).replace("\\", "/")
|
|
49
|
+
for node in ast.walk(tree):
|
|
50
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
51
|
+
end = node.end_lineno or node.lineno
|
|
52
|
+
line_count = end - node.lineno + 1
|
|
53
|
+
if line_count >= min_lines:
|
|
54
|
+
large.append({"file": rel, "function": node.name, "lines": line_count, "line_start": node.lineno})
|
|
55
|
+
if len(large) >= max_results:
|
|
56
|
+
break
|
|
57
|
+
except (SyntaxError, OSError):
|
|
58
|
+
continue
|
|
59
|
+
if len(large) >= max_results:
|
|
60
|
+
break
|
|
61
|
+
if not large:
|
|
62
|
+
return f"No functions with >= {min_lines} lines found."
|
|
63
|
+
import json
|
|
64
|
+
|
|
65
|
+
return json.dumps({"large_functions": large}, indent=2)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
register(LargeFunctionDetectorTool())
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Map module responsibility using docstrings and top-level exports."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModuleResponsibilityMapperTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Map each module to a short responsibility summary (docstring or first line).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "module_responsibility_mapper"
|
|
16
|
+
description = "Map modules to responsibility summaries (docstring/first line)."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Root path"},
|
|
21
|
+
"max_modules": {"type": "integer", "description": "Max modules (default 50)"},
|
|
22
|
+
},
|
|
23
|
+
"required": ["path"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def run(self, **kwargs) -> str:
|
|
27
|
+
path = kwargs.get("path")
|
|
28
|
+
max_modules = kwargs.get("max_modules", 50)
|
|
29
|
+
if not path or not isinstance(path, str):
|
|
30
|
+
return "Error: path must be a non-empty string"
|
|
31
|
+
if not isinstance(max_modules, int) or max_modules < 1:
|
|
32
|
+
max_modules = 50
|
|
33
|
+
root = Path(path).resolve()
|
|
34
|
+
if not root.exists() or not root.is_dir():
|
|
35
|
+
return f"Error: path must be an existing directory: {path}"
|
|
36
|
+
results = []
|
|
37
|
+
for p in root.rglob("*.py"):
|
|
38
|
+
if len(results) >= max_modules:
|
|
39
|
+
break
|
|
40
|
+
if not p.is_file():
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
44
|
+
tree = ast.parse(text)
|
|
45
|
+
doc = ast.get_docstring(tree) or ""
|
|
46
|
+
first = text.strip().split("\n")[0][:60] if text.strip() else ""
|
|
47
|
+
summary = (doc.split("\n")[0] if doc else first).strip() or "(no summary)"
|
|
48
|
+
results.append({"module": str(p.relative_to(root)).replace("\\", "/"), "responsibility": summary[:200]})
|
|
49
|
+
except (SyntaxError, OSError):
|
|
50
|
+
continue
|
|
51
|
+
import json
|
|
52
|
+
|
|
53
|
+
return json.dumps({"modules": results}, indent=2)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
register(ModuleResponsibilityMapperTool())
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Split codebase analysis into batches for parallel/swarm processing."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ParallelCodebaseAnalysisTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Split a codebase into file batches for parallel analysis (e.g. by swarm workers).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "parallel_codebase_analysis"
|
|
16
|
+
description = "Split codebase into batches for parallel analysis. Returns batch plan."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Root path"},
|
|
21
|
+
"batch_size": {"type": "integer", "description": "Files per batch (default 10)"},
|
|
22
|
+
"extension": {"type": "string", "description": "File extension (default .py)"},
|
|
23
|
+
},
|
|
24
|
+
"required": ["path"],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def run(self, **kwargs) -> str:
|
|
28
|
+
path = kwargs.get("path")
|
|
29
|
+
batch_size = kwargs.get("batch_size", 10)
|
|
30
|
+
extension = kwargs.get("extension", ".py")
|
|
31
|
+
if not path or not isinstance(path, str):
|
|
32
|
+
return "Error: path must be a non-empty string"
|
|
33
|
+
if not isinstance(batch_size, int) or batch_size < 1:
|
|
34
|
+
batch_size = 10
|
|
35
|
+
root = Path(path).resolve()
|
|
36
|
+
if not root.exists() or not root.is_dir():
|
|
37
|
+
return f"Error: path must be an existing directory: {path}"
|
|
38
|
+
files = [str(p.relative_to(root)).replace("\\", "/") for p in root.rglob(f"*{extension}") if p.is_file()]
|
|
39
|
+
batches = [files[i : i + batch_size] for i in range(0, len(files), batch_size)]
|
|
40
|
+
result = {"root": str(root), "total_files": len(files), "batch_size": batch_size, "num_batches": len(batches), "batches": batches}
|
|
41
|
+
return json.dumps(result, indent=2)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
register(ParallelCodebaseAnalysisTool())
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Detect refactor candidates: long functions, high branch count, deep nesting."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RefactorCandidateDetectorTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Detect refactor candidates: functions with many lines, many branches, or deep nesting.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "refactor_candidate_detector"
|
|
16
|
+
description = "Detect refactor candidates: long functions, high complexity, deep nesting."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Path to file or directory"},
|
|
21
|
+
"min_lines": {"type": "integer", "description": "Flag functions with >= N lines (default 30)"},
|
|
22
|
+
"min_branches": {"type": "integer", "description": "Flag functions with >= N branches (default 8)"},
|
|
23
|
+
},
|
|
24
|
+
"required": ["path"],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def _complexity(self, node: ast.AST) -> tuple[int, int]:
|
|
28
|
+
branches = 0
|
|
29
|
+
max_nest = 0
|
|
30
|
+
|
|
31
|
+
def visit(n: ast.AST, depth: int) -> None:
|
|
32
|
+
nonlocal branches, max_nest
|
|
33
|
+
max_nest = max(max_nest, depth)
|
|
34
|
+
if isinstance(n, (ast.If, ast.While, ast.For, ast.With, ast.Try, ast.Assert)):
|
|
35
|
+
branches += 1
|
|
36
|
+
if isinstance(n, ast.comprehension):
|
|
37
|
+
branches += 1
|
|
38
|
+
for c in ast.iter_child_nodes(n):
|
|
39
|
+
visit(c, depth + 1)
|
|
40
|
+
|
|
41
|
+
visit(node, 0)
|
|
42
|
+
return branches, max_nest
|
|
43
|
+
|
|
44
|
+
def run(self, **kwargs) -> str:
|
|
45
|
+
path = kwargs.get("path")
|
|
46
|
+
min_lines = kwargs.get("min_lines", 30)
|
|
47
|
+
min_branches = kwargs.get("min_branches", 8)
|
|
48
|
+
if not path or not isinstance(path, str):
|
|
49
|
+
return "Error: path must be a non-empty string"
|
|
50
|
+
if not isinstance(min_lines, int) or min_lines < 1:
|
|
51
|
+
min_lines = 30
|
|
52
|
+
if not isinstance(min_branches, int) or min_branches < 1:
|
|
53
|
+
min_branches = 8
|
|
54
|
+
p = Path(path).resolve()
|
|
55
|
+
if not p.exists():
|
|
56
|
+
return f"Error: path not found: {path}"
|
|
57
|
+
files = [p] if p.is_file() and p.suffix == ".py" else list(p.rglob("*.py")) if p.is_dir() else []
|
|
58
|
+
candidates = []
|
|
59
|
+
for f in files:
|
|
60
|
+
if not f.is_file():
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
64
|
+
tree = ast.parse(text)
|
|
65
|
+
lines = text.splitlines()
|
|
66
|
+
rel = str(f.relative_to(p if p.is_dir() else f.parent)).replace("\\", "/")
|
|
67
|
+
for node in ast.walk(tree):
|
|
68
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
69
|
+
start, end = node.lineno, node.end_lineno or node.lineno
|
|
70
|
+
line_count = end - start + 1
|
|
71
|
+
branches, nest = self._complexity(node)
|
|
72
|
+
if line_count >= min_lines or branches >= min_branches:
|
|
73
|
+
candidates.append({"file": rel, "name": node.name, "lines": line_count, "branches": branches, "max_nesting": nest})
|
|
74
|
+
except (SyntaxError, OSError):
|
|
75
|
+
continue
|
|
76
|
+
import json
|
|
77
|
+
|
|
78
|
+
return json.dumps({"candidates": candidates[:40]}, indent=2)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
register(RefactorCandidateDetectorTool())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Build a semantic-style index: modules and their docstrings / first line summary."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from devsper.tools.base import Tool
|
|
7
|
+
from devsper.tools.registry import register
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RepositorySemanticIndexTool(Tool):
|
|
11
|
+
"""
|
|
12
|
+
Index modules with docstrings and first-line summaries for semantic search (keyword match).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
name = "repository_semantic_index"
|
|
16
|
+
description = "Build a semantic index of a repo: module docstrings and summaries."
|
|
17
|
+
input_schema = {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Root path to index"},
|
|
21
|
+
"max_files": {"type": "integer", "description": "Max files to index (default 100)"},
|
|
22
|
+
},
|
|
23
|
+
"required": ["path"],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def _extract_summary(self, p: Path) -> dict:
|
|
27
|
+
try:
|
|
28
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
29
|
+
tree = ast.parse(text)
|
|
30
|
+
doc = ast.get_docstring(tree)
|
|
31
|
+
first_line = text.strip().split("\n")[0][:80] if text.strip() else ""
|
|
32
|
+
return {"docstring": (doc or "")[:300], "first_line": first_line}
|
|
33
|
+
except (SyntaxError, OSError):
|
|
34
|
+
return {"docstring": "", "first_line": ""}
|
|
35
|
+
|
|
36
|
+
def run(self, **kwargs) -> str:
|
|
37
|
+
path = kwargs.get("path")
|
|
38
|
+
max_files = kwargs.get("max_files", 100)
|
|
39
|
+
if not path or not isinstance(path, str):
|
|
40
|
+
return "Error: path must be a non-empty string"
|
|
41
|
+
if not isinstance(max_files, int) or max_files < 1:
|
|
42
|
+
max_files = 100
|
|
43
|
+
root = Path(path).resolve()
|
|
44
|
+
if not root.exists() or not root.is_dir():
|
|
45
|
+
return f"Error: path must be an existing directory: {path}"
|
|
46
|
+
entries = []
|
|
47
|
+
for p in root.rglob("*.py"):
|
|
48
|
+
if len(entries) >= max_files:
|
|
49
|
+
break
|
|
50
|
+
if not p.is_file():
|
|
51
|
+
continue
|
|
52
|
+
rel = p.relative_to(root)
|
|
53
|
+
summary = self._extract_summary(p)
|
|
54
|
+
if summary["docstring"] or summary["first_line"]:
|
|
55
|
+
entries.append({"file": str(rel), **summary})
|
|
56
|
+
import json
|
|
57
|
+
|
|
58
|
+
return json.dumps({"root": str(root), "entries": entries}, indent=2)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
register(RepositorySemanticIndexTool())
|