refactorika 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- refactorika/__init__.py +3 -0
- refactorika/agents/__init__.py +0 -0
- refactorika/agents/base.py +23 -0
- refactorika/agents/complexity_agent.py +28 -0
- refactorika/agents/dead_code_agent.py +23 -0
- refactorika/agents/duplicate_agent.py +27 -0
- refactorika/agents/import_agent.py +15 -0
- refactorika/agents/orchestrator.py +82 -0
- refactorika/analysis/__init__.py +0 -0
- refactorika/analysis/audit.py +86 -0
- refactorika/analysis/call_graph.py +411 -0
- refactorika/analysis/dead_code.py +248 -0
- refactorika/analysis/duplicates.py +337 -0
- refactorika/analysis/embeddings.py +164 -0
- refactorika/analysis/parser.py +129 -0
- refactorika/analysis/related.py +159 -0
- refactorika/cli.py +382 -0
- refactorika/core/__init__.py +1 -0
- refactorika/core/analyze.py +137 -0
- refactorika/core/apply.py +161 -0
- refactorika/core/gates.py +126 -0
- refactorika/core/schema.py +275 -0
- refactorika/core/storage.py +157 -0
- refactorika/dashboard.py +165 -0
- refactorika/docs_gen.py +286 -0
- refactorika/harness.py +266 -0
- refactorika/languages/__init__.py +18 -0
- refactorika/languages/base.py +45 -0
- refactorika/languages/generic_adapter.py +18 -0
- refactorika/languages/python_adapter.py +49 -0
- refactorika/languages/registry.py +29 -0
- refactorika/mcp_server.py +193 -0
- refactorika/memory/__init__.py +0 -0
- refactorika/memory/agent_memory.py +116 -0
- refactorika/memory/context.py +113 -0
- refactorika/memory/vector_index.py +325 -0
- refactorika/observability.py +152 -0
- refactorika/transforms/__init__.py +0 -0
- refactorika/transforms/dead.py +94 -0
- refactorika/transforms/imports.py +95 -0
- refactorika-0.2.0.dist-info/METADATA +541 -0
- refactorika-0.2.0.dist-info/RECORD +45 -0
- refactorika-0.2.0.dist-info/WHEEL +4 -0
- refactorika-0.2.0.dist-info/entry_points.txt +3 -0
- refactorika-0.2.0.dist-info/licenses/LICENSE +21 -0
refactorika/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Base class for all specialist refactor agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from ..core.apply import apply_and_verify
|
|
8
|
+
from ..core.schema import EditRecord, PlanTask
|
|
9
|
+
from ..core.storage import Storage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SpecialistAgent(ABC):
|
|
13
|
+
supported_kinds: list[str] = []
|
|
14
|
+
|
|
15
|
+
def handle(self, task: PlanTask, storage: Storage) -> EditRecord:
|
|
16
|
+
new_content = self.propose(task, storage)
|
|
17
|
+
dominant_kind = (
|
|
18
|
+
task.opportunities[0].kind if task.opportunities else self.supported_kinds[0]
|
|
19
|
+
)
|
|
20
|
+
return apply_and_verify(task.file, new_content, dominant_kind, storage)
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def propose(self, task: PlanTask, storage: Storage) -> str: ...
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""ComplexityAgent: handles function splits, nesting flattening, helper extraction.
|
|
2
|
+
|
|
3
|
+
propose() is a stub — wire in LLM reasoning to generate new_content.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..core.schema import PlanTask
|
|
11
|
+
from ..core.storage import Storage
|
|
12
|
+
from .base import SpecialistAgent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ComplexityAgent(SpecialistAgent):
|
|
16
|
+
supported_kinds = [
|
|
17
|
+
"split_function",
|
|
18
|
+
"flatten_nesting",
|
|
19
|
+
"extract_helper",
|
|
20
|
+
"split_module",
|
|
21
|
+
"dedupe_block",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def propose(self, task: PlanTask, storage: Storage) -> str:
|
|
25
|
+
# Stub: returns original content (no-op) until LLM reasoning is wired in.
|
|
26
|
+
# Replace with an LLM call that receives task.opportunities + file content
|
|
27
|
+
# and returns refactored source.
|
|
28
|
+
return Path(task.file).read_text()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""DeadCodeAgent: removes high-confidence dead symbols (deterministic, no LLM needed)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..core.schema import PlanTask
|
|
6
|
+
from ..core.storage import Storage
|
|
7
|
+
from .base import SpecialistAgent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DeadCodeAgent(SpecialistAgent):
|
|
11
|
+
supported_kinds = ["remove_dead_code"]
|
|
12
|
+
|
|
13
|
+
def propose(self, task: PlanTask, storage: Storage) -> str:
|
|
14
|
+
from ..analysis.dead_code import find_dead_code
|
|
15
|
+
from ..transforms.dead import remove_dead_symbols
|
|
16
|
+
|
|
17
|
+
result = find_dead_code(task.file, storage)
|
|
18
|
+
high_conf_names = {
|
|
19
|
+
s["name"].split(".")[-1]
|
|
20
|
+
for s in result.get("dead_symbols", [])
|
|
21
|
+
if s["confidence"] == "high"
|
|
22
|
+
}
|
|
23
|
+
return remove_dead_symbols(task.file, high_conf_names)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""DuplicateAgent: consolidates duplicate functions.
|
|
2
|
+
|
|
3
|
+
propose() is a stub — wire in LLM reasoning to generate merged new_content.
|
|
4
|
+
Multi-file consolidation overrides handle() to use apply_and_verify_multi.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..core.schema import EditRecord, PlanTask
|
|
12
|
+
from ..core.storage import Storage
|
|
13
|
+
from .base import SpecialistAgent
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DuplicateAgent(SpecialistAgent):
|
|
17
|
+
supported_kinds = ["consolidate_duplicate"]
|
|
18
|
+
|
|
19
|
+
def handle(self, task: PlanTask, storage: Storage) -> EditRecord:
|
|
20
|
+
# Stub delegates to single-file base path.
|
|
21
|
+
# Override here to call apply_and_verify_multi once LLM wiring provides
|
|
22
|
+
# new content for both files in a duplicate pair.
|
|
23
|
+
return super().handle(task, storage)
|
|
24
|
+
|
|
25
|
+
def propose(self, task: PlanTask, storage: Storage) -> str:
|
|
26
|
+
# Stub: returns original content (no-op) until LLM reasoning is wired in.
|
|
27
|
+
return Path(task.file).read_text()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""ImportAgent: reorders and deduplicates imports (deterministic, no LLM needed)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..core.schema import PlanTask
|
|
6
|
+
from ..core.storage import Storage
|
|
7
|
+
from .base import SpecialistAgent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImportAgent(SpecialistAgent):
|
|
11
|
+
supported_kinds = ["reorder_imports"]
|
|
12
|
+
|
|
13
|
+
def propose(self, task: PlanTask, storage: Storage) -> str:
|
|
14
|
+
from ..transforms.imports import reorder_imports
|
|
15
|
+
return reorder_imports(task.file)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Dispatch confirmed-plan tasks to specialists in dependency-ordered waves."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
|
|
8
|
+
from ..core.schema import Plan, PlanTask
|
|
9
|
+
from ..core.storage import Storage
|
|
10
|
+
from .base import SpecialistAgent
|
|
11
|
+
from .complexity_agent import ComplexityAgent
|
|
12
|
+
from .dead_code_agent import DeadCodeAgent
|
|
13
|
+
from .duplicate_agent import DuplicateAgent
|
|
14
|
+
from .import_agent import ImportAgent
|
|
15
|
+
|
|
16
|
+
_SPECIALISTS: list[SpecialistAgent] = [
|
|
17
|
+
ImportAgent(),
|
|
18
|
+
DeadCodeAgent(),
|
|
19
|
+
ComplexityAgent(),
|
|
20
|
+
DuplicateAgent(),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _route(task: PlanTask) -> SpecialistAgent | None:
|
|
25
|
+
"""Return the specialist that handles the task's dominant refactor_kind."""
|
|
26
|
+
if not task.opportunities:
|
|
27
|
+
return None
|
|
28
|
+
dominant = task.opportunities[0].kind
|
|
29
|
+
for s in _SPECIALISTS:
|
|
30
|
+
if dominant in s.supported_kinds:
|
|
31
|
+
return s
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def dispatch_plan(storage: Storage, max_workers: int = 4) -> dict:
|
|
36
|
+
"""Read the confirmed plan, dispatch tasks in dependency-ordered waves, return a summary."""
|
|
37
|
+
raw = storage.load_plan()
|
|
38
|
+
if raw is None:
|
|
39
|
+
return {"error": "no plan; call get_plan first"}
|
|
40
|
+
plan = Plan.from_dict(raw)
|
|
41
|
+
if not plan.confirmed:
|
|
42
|
+
return {"error": "plan not confirmed; call confirm_plan first"}
|
|
43
|
+
|
|
44
|
+
by_order: dict[int, list[PlanTask]] = defaultdict(list)
|
|
45
|
+
for task in plan.tasks:
|
|
46
|
+
by_order[task.order].append(task)
|
|
47
|
+
|
|
48
|
+
committed, rolled_back, skipped = 0, 0, 0
|
|
49
|
+
records: list[dict] = []
|
|
50
|
+
|
|
51
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
52
|
+
for level in sorted(by_order.keys()):
|
|
53
|
+
wave = by_order[level]
|
|
54
|
+
futures = {}
|
|
55
|
+
for task in wave:
|
|
56
|
+
agent = _route(task)
|
|
57
|
+
if agent is None:
|
|
58
|
+
skipped += 1
|
|
59
|
+
continue
|
|
60
|
+
futures[executor.submit(agent.handle, task, storage)] = task
|
|
61
|
+
|
|
62
|
+
for future in as_completed(futures):
|
|
63
|
+
try:
|
|
64
|
+
record = future.result()
|
|
65
|
+
records.append(record.to_dict())
|
|
66
|
+
if record.status == "committed":
|
|
67
|
+
committed += 1
|
|
68
|
+
elif record.status == "rolled-back":
|
|
69
|
+
rolled_back += 1
|
|
70
|
+
else:
|
|
71
|
+
skipped += 1
|
|
72
|
+
except Exception as exc: # noqa: BLE001
|
|
73
|
+
task = futures[future]
|
|
74
|
+
records.append({"file": task.file, "error": str(exc)})
|
|
75
|
+
skipped += 1
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"committed": committed,
|
|
79
|
+
"rolled_back": rolled_back,
|
|
80
|
+
"skipped": skipped,
|
|
81
|
+
"records": records,
|
|
82
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Repo-wide audit + dependency-ordered planning (advisory, read-only).
|
|
2
|
+
|
|
3
|
+
Reuses the existing per-file analysis (`analyze_file`) and call graph
|
|
4
|
+
(`CallGraph`) — no new analysis logic. The audit aggregates opportunities into a
|
|
5
|
+
ranked report; the plan orders deviating files fewest-dependents-first so
|
|
6
|
+
low-blast-radius edits land before the modules many things depend on.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections import Counter, defaultdict
|
|
12
|
+
|
|
13
|
+
from refactorika.analysis.call_graph import CallGraph, _collect_py_files, _module_name
|
|
14
|
+
from refactorika.core.analyze import analyze_file
|
|
15
|
+
from refactorika.core.schema import AuditEntry, Plan, PlanTask, RepoAudit
|
|
16
|
+
from refactorika.core.storage import Storage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def audit_repo(path: str, storage: Storage) -> RepoAudit:
|
|
20
|
+
"""Walk the repo, aggregate per-file opportunities into a ranked report."""
|
|
21
|
+
files, _root = _collect_py_files(path)
|
|
22
|
+
entries: list[AuditEntry] = []
|
|
23
|
+
by_kind: Counter = Counter()
|
|
24
|
+
rank_by_kind: defaultdict[str, int] = defaultdict(int)
|
|
25
|
+
total = 0
|
|
26
|
+
|
|
27
|
+
for f in files:
|
|
28
|
+
opps = analyze_file(str(f), storage).opportunities
|
|
29
|
+
if not opps:
|
|
30
|
+
continue # a "deviating" file is one with >= 1 opportunity
|
|
31
|
+
total += len(opps)
|
|
32
|
+
for o in opps:
|
|
33
|
+
by_kind[o.kind] += 1
|
|
34
|
+
rank_by_kind[o.kind] += o.rank
|
|
35
|
+
entries.append(
|
|
36
|
+
AuditEntry(file=str(f), opportunities=opps, score=sum(o.rank for o in opps))
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
entries.sort(key=lambda e: e.score, reverse=True)
|
|
40
|
+
|
|
41
|
+
dominant = None
|
|
42
|
+
if rank_by_kind: # the kind worth doing most across the repo (highest summed rank)
|
|
43
|
+
top_kind = max(rank_by_kind, key=lambda k: rank_by_kind[k])
|
|
44
|
+
dominant = f"{top_kind} ({by_kind[top_kind]} sites)"
|
|
45
|
+
|
|
46
|
+
return RepoAudit(
|
|
47
|
+
repo=path,
|
|
48
|
+
files_scanned=len(files),
|
|
49
|
+
total_opportunities=total,
|
|
50
|
+
by_kind=dict(by_kind),
|
|
51
|
+
dominant_finding=dominant,
|
|
52
|
+
entries=entries,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_plan(path: str, storage: Storage) -> Plan:
|
|
57
|
+
"""Order deviating files fewest-dependents-first; persist + return the plan."""
|
|
58
|
+
audit = audit_repo(path, storage)
|
|
59
|
+
files, root = _collect_py_files(path)
|
|
60
|
+
cg = CallGraph.build(path)
|
|
61
|
+
module_of = {str(f): _module_name(f, root) for f in files}
|
|
62
|
+
|
|
63
|
+
tasks: list[PlanTask] = []
|
|
64
|
+
for entry in audit.entries: # only deviating files (>=1 opportunity)
|
|
65
|
+
module = module_of.get(entry.file, "")
|
|
66
|
+
deps = cg.dependents_of(module) if module else []
|
|
67
|
+
tasks.append(
|
|
68
|
+
PlanTask(
|
|
69
|
+
file=entry.file,
|
|
70
|
+
opportunities=entry.opportunities,
|
|
71
|
+
dependents=deps,
|
|
72
|
+
order=0,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Fewest-dependents-first (low blast radius first); tie-break by score desc.
|
|
77
|
+
def _order_key(t: PlanTask) -> tuple[int, int]:
|
|
78
|
+
return (len(t.dependents), -sum(o.rank for o in t.opportunities))
|
|
79
|
+
|
|
80
|
+
tasks.sort(key=_order_key)
|
|
81
|
+
for i, t in enumerate(tasks):
|
|
82
|
+
t.order = i
|
|
83
|
+
|
|
84
|
+
plan = Plan(repo=path, dominant_finding=audit.dominant_finding, tasks=tasks)
|
|
85
|
+
storage.save_plan(plan.to_dict())
|
|
86
|
+
return plan
|