Metadata-Version: 2.4
Name: surgraft
Version: 0.1.0
Summary: Surgical code grafting — migrate functions between files with zero LLM token waste
Project-URL: Homepage, https://github.com/mayaanhafeez/surgraft
Project-URL: Repository, https://github.com/mayaanhafeez/surgraft
Project-URL: Issues, https://github.com/mayaanhafeez/surgraft/issues
Author-email: Ayaan Hafeez <m.ayaan.hafeez@gmail.com>
License: MIT
License-File: LICENSE
Keywords: anthropic,ast,cli,code-migration,llm,refactoring
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Requires-Dist: click>=8.0
Provides-Extra: dev
Requires-Dist: anthropic>=0.20; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Provides-Extra: llm
Requires-Dist: anthropic>=0.20; extra == 'llm'
Description-Content-Type: text/markdown

# surgraft

**Surgical code grafting. Spend tokens only on what changed.**

When refactoring a large file, most LLM tools read a function and then *rewrite* it into the new file — burning tokens on code that didn't change. `surgraft` separates the two concerns:

1. **Locate** — find function boundaries using AST parsing (zero tokens)
2. **Copy** — shell-level byte copy into the new file (zero tokens)  
3. **Edit** — LLM sees only the extracted snippet (tokens ∝ delta, not file size)

```
5,000-line file, 20-line function, naive LLM rewrite: ~5,000 tokens
surgraft copy-only:                                       0 tokens
surgraft + edit pass:                                   ~25 tokens
```

---

## Install

```bash
pip install surgraft                   # copy-only (no API key needed)
pip install "surgraft[llm]"            # + LLM edit pass via Anthropic
```

---

## Usage

### List symbols in a file

```bash
surgraft list old_service.py
```

```
old_service.py  (4,821 lines)

  SYMBOL                                   KIND          LINES
  ────────────────────────────────────────────────────────────
  AuthService                              class         42–310   (269L)
  AuthService.validate_token               function      58–89    (32L)
  AuthService.refresh_session              function      91–134   (44L)
  parse_jwt                                function      312–341  (30L)
  hash_password                            function      343–367  (25L)
  ...

  23 symbols
```

---

### Graft symbols — zero tokens

Copy functions into a new file without touching the LLM at all:

```bash
surgraft graft old_service.py new_auth.py \
  --symbols "validate_token,refresh_session,parse_jwt"
```

```
→ Source   old_service.py
→ Dest     new_auth.py

✓ Grafted   validate_token    lines 58–89    → new_auth.py
✓ Grafted   refresh_session   lines 91–134   → new_auth.py
✓ Grafted   parse_jwt         lines 312–341  → new_auth.py

  Tokens spent on copy: 0  (naive rewrite ≈ 19,284 tokens)
```

---

### Graft + edit — tokens only on the delta

Copy and transform in one step. The LLM sees only the extracted snippet:

```bash
surgraft graft old_service.py new_auth.py \
  --symbols "validate_token,refresh_session" \
  --edit "convert to async/await, replace self.db with db: AsyncSession parameter"
```

```
→ Source   old_service.py
→ Dest     new_auth.py
→ Edit     'convert to async/await...'

→ Editing  validate_token    (~142 tokens)
✓ Grafted (edited)   validate_token  → new_auth.py
→ Editing  refresh_session   (~176 tokens)
✓ Grafted (edited)   refresh_session → new_auth.py

  Token usage: 318 tokens used  (naive rewrite ≈ 19,284 tokens, saved ~98%)
```

---

### Graft all symbols

```bash
surgraft graft old_service.py new_service.py --symbols ALL
```

---

### Check migration progress

See what's been moved and what's still outstanding:

```bash
surgraft diff old_service.py new_service.py
```

```
  Migration gap: old_service.py → new_service.py

  NOT YET GRAFTED
    ✗ parse_jwt                              30L
    ✗ hash_password                          25L
    ✗ AuthService.rotate_key                 18L

  ALREADY PRESENT
    ✓ validate_token
    ✓ refresh_session

  3 remaining / 2 done / 5 total
```

---

### Dry run

Preview what would be copied without writing anything:

```bash
surgraft graft old.py new.py --symbols parse_jwt --dry-run
```

---

## How it works

```
┌─────────────────────────────────────────────────────────┐
│                      surgraft graft                      │
└────────────────────┬────────────────────────────────────┘
                     │
         ┌───────────▼───────────┐
         │   AST / regex parser  │  ← zero tokens
         │  finds line boundaries │
         └───────────┬───────────┘
                     │
         ┌───────────▼───────────┐
         │    Shell byte copy    │  ← zero tokens
         │  sed -n 'N,Mp' → dest │
         └───────────┬───────────┘
                     │
              --edit provided?
                     │
          YES ───────▼────────  NO → done
         ┌───────────────────┐
         │  LLM edit pass    │  ← tokens ∝ snippet, not file
         │  sees: snippet    │
         │  + instruction    │
         │  outputs: snippet │
         └───────────────────┘
```

**Supported languages**

| Language | Method | Accuracy |
|---|---|---|
| Python | `ast` module | Exact |
| JavaScript / TypeScript | Regex heuristic | Good |
| JSX / TSX | Regex heuristic | Good |

---

## API

Use `surgraft` as a library:

```python
from surgraft.extractor import extract_symbols, find_symbol, graft_lines, read_lines
from surgraft.llm import edit_snippet

# find all symbols
symbols = extract_symbols("old_service.py")

# locate one
sym = find_symbol("old_service.py", "validate_token")
print(sym.start_line, sym.end_line)  # 58 89

# copy — zero tokens
graft_lines("old_service.py", sym.start_line, sym.end_line, "new_auth.py")

# edit — tokens ∝ snippet only
snippet = read_lines("old_service.py", sym.start_line, sym.end_line)
edited = edit_snippet(snippet, "make this async")

with open("new_auth.py", "a") as f:
    f.write(edited)
```

---

## Configuration

Set your API key for the edit pass:

```bash
export ANTHROPIC_API_KEY=sk-ant-...
```

Or pass it inline:

```bash
surgraft graft old.py new.py --symbols fn --edit "..." --api-key sk-ant-...
```

---

## Why not just use an LLM agent?

LLM agents do solve refactoring — but they solve it by reading and rewriting. For a 5,000-line file with 50 functions, that's 50 × ~5,000 = 250,000 tokens spent transcribing unchanged code. `surgraft` treats the LLM as a *transformer*, not a *copier*. The bytes that didn't change never touch the model.

---

## Contributing

```bash
git clone https://github.com/your-name/surgraft
cd surgraft
pip install -e ".[dev]"
pytest
```

---

## License

MIT
