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.
Files changed (45) hide show
  1. refactorika/__init__.py +3 -0
  2. refactorika/agents/__init__.py +0 -0
  3. refactorika/agents/base.py +23 -0
  4. refactorika/agents/complexity_agent.py +28 -0
  5. refactorika/agents/dead_code_agent.py +23 -0
  6. refactorika/agents/duplicate_agent.py +27 -0
  7. refactorika/agents/import_agent.py +15 -0
  8. refactorika/agents/orchestrator.py +82 -0
  9. refactorika/analysis/__init__.py +0 -0
  10. refactorika/analysis/audit.py +86 -0
  11. refactorika/analysis/call_graph.py +411 -0
  12. refactorika/analysis/dead_code.py +248 -0
  13. refactorika/analysis/duplicates.py +337 -0
  14. refactorika/analysis/embeddings.py +164 -0
  15. refactorika/analysis/parser.py +129 -0
  16. refactorika/analysis/related.py +159 -0
  17. refactorika/cli.py +382 -0
  18. refactorika/core/__init__.py +1 -0
  19. refactorika/core/analyze.py +137 -0
  20. refactorika/core/apply.py +161 -0
  21. refactorika/core/gates.py +126 -0
  22. refactorika/core/schema.py +275 -0
  23. refactorika/core/storage.py +157 -0
  24. refactorika/dashboard.py +165 -0
  25. refactorika/docs_gen.py +286 -0
  26. refactorika/harness.py +266 -0
  27. refactorika/languages/__init__.py +18 -0
  28. refactorika/languages/base.py +45 -0
  29. refactorika/languages/generic_adapter.py +18 -0
  30. refactorika/languages/python_adapter.py +49 -0
  31. refactorika/languages/registry.py +29 -0
  32. refactorika/mcp_server.py +193 -0
  33. refactorika/memory/__init__.py +0 -0
  34. refactorika/memory/agent_memory.py +116 -0
  35. refactorika/memory/context.py +113 -0
  36. refactorika/memory/vector_index.py +325 -0
  37. refactorika/observability.py +152 -0
  38. refactorika/transforms/__init__.py +0 -0
  39. refactorika/transforms/dead.py +94 -0
  40. refactorika/transforms/imports.py +95 -0
  41. refactorika-0.2.0.dist-info/METADATA +541 -0
  42. refactorika-0.2.0.dist-info/RECORD +45 -0
  43. refactorika-0.2.0.dist-info/WHEEL +4 -0
  44. refactorika-0.2.0.dist-info/entry_points.txt +3 -0
  45. refactorika-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """Refactorika — verified structural refactoring for Python, exposed to Claude over MCP."""
2
+
3
+ __version__ = "0.2.0"
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