Skip to main content
Glama
README.md
# surgical ✂️

**Enforce Karpathy's Rule #3. Automatically prune AI diff bloat and over-editing before you commit.**

When you ask an AI coding agent to fix a 2-line bug, research by Tongyao Zhu, Wei Hern Lim, and Min-Yen Kan shows it routinely rewrites 40–100 lines: renaming variables in unrelated files, reordering imports, and touching working code you never asked it to touch ([arXiv:2609.04061](https://arxiv.org/abs/2609.04061)). That over-editing shows up in **any** stack—Next.js, FastAPI, Go, Rust, SQL migrations—especially on generic names like `page.tsx`, `main.py`, `app.py`, `utils.ts`, and `lib.rs`.

`surgical` is a language-agnostic MCP server and CLI. It audits uncommitted Git changes against your stated intent using **Universal Lexical Topology** (sub-word tokens, an identifier graph, and cosmetic normalization)—not per-language parsers or fragile filenames—and strips unrequested drive-by refactors before you commit.

Zero network dependencies. Operates 100% locally on your Git working tree in `<5ms`. No Python runtime, no tree-sitter, no extra language tooling.

*(Currently in pre-release testing — install directly from source)*.

---

## The Rule We Enforce

> **Andrej Karpathy's Rule #3 for Coding Agents:**  
> *"Surgical Changes Only — touch only what was asked, never refactor adjacent code, one diff one scope."*

---

## Why Filename Heuristics Fail

Most naive diff cleaners check filenames against keywords in your prompt. That breaks immediately on modern apps:
* In the **Next.js App Router**, almost every file is named `page.tsx`, `layout.tsx`, or `route.ts`.
* Shared logic lives in `utils.ts`, `helpers.ts`, `lib.rs`, `utils.py`, or `helpers.go`.
* In **Python** services (FastAPI, Django, Flask), the same trap is `main.py`, `app.py`, `models.py`, and `config.py`.

If your prompt is *"Fix the mobile navbar"*, a filename matcher will blindly discard `app/page.tsx` and break your build. If it is *"Add a `/healthz` endpoint"*, the same matcher will drop `main.py` or keep an unrelated `models.py` because the filename looks "important."

### How `surgical` beats filenames (Universal Lexical Topology)

1. **Sub-word tokenizer:** camelCase, PascalCase, snake_case, kebab-case, and SCREAMING_SNAKE are split into stems. Prompt *"Fix mobile navbar toggle"* → `{mobile, navbar, toggle}`. That matches `NavbarToggle`, `navbar_toggle`, and `toggle_nav()` in TypeScript, Python, Go, or Rust.
2. **Identifier graph (no import parsing):** Novel identifiers are taken from **seed hunks** that match the intent. Files that share those identifiers are **KEEP**. Files with zero overlap to the seed cluster are **BLOAT**.
3. **Cosmetic normalization:** Comments (`//`, `#`, `--`, `/* */`, `""" """`, `''' '''`) and whitespace are stripped. If before equals after, the hunk is formatter churn (black / isort / prettier / gofmt / rustfmt) and is pruned even if it contains identifiers.

Generic-file examples (`main.py`, `app.py`, `page.tsx`, `utils.ts`) are scored by hunk content and identifier overlap—not by path.

### FastAPI example (same engine, any language)

**Intent:** *Add a `/healthz` endpoint.*

| Change | Verdict | Why |
| :--- | :--- | :--- |
| `main.py` — `@app.get("/healthz") def health_check(): return {"status": "ok"}` | **Keep (surgical)** | Sub-word tokens match `healthz`, even though the filename is generic. |
| `utils/db.py` — helper `health_check` uses (`check_db_connection`) | **Keep (connected)** | Identifier graph: the endpoint and helper share identifiers. |
| `routers/auth.py` — unsolicited JWT validation refactor + import reorder | **Prune (bloat)** | Isolated island; zero identifier overlap with the `/healthz` seed cluster. |
| `config.py` — Black formatting-only cosmetic changes | **Prune (cosmetic)** | After comment/whitespace strip, before == after. |

A polyglot commit is the same: Python `navbar_toggle`, Go `NavbarToggle`, and TypeScript `NavbarToggle` stay; an unrelated SQL index and prettier-only CSS are pruned.

---

## 1. Quick Setup (From Source)

Requires **Node.js 18+** and Git.

```bash
git clone https://github.com/preprint-labs/surgical.git
cd surgical
npm install
npm run build
```

Verify that `dist/index.js` was created.

---

## 2. Editor Setup (Cursor, Claude Code, GitHub Copilot)

Because you built from source, point your editor to your local `dist/index.js` binary.

### Find your full script path:
* **Windows (PowerShell):** `(Resolve-Path dist\index.js).Path`
* **macOS / Linux:** `pwd`/`dist/index.js`

---

### A. Cursor Setup

#### Option 1: Workspace Config (Recommended)
Add to your project's `.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "surgical": {
      "command": "node",
      "args": ["<FULL_PATH_TO_SURGICAL>/dist/index.js"]
    }
  }
}
```
*(On Windows, use escaped backslashes: `"C:\\path\\to\\surgical\\dist\\index.js"`)*.

#### Option 2: Cursor GUI
1. Open **Settings** (`Ctrl + Shift + J` or `Cmd + Shift + J`) -> **Features** -> **MCP**.
2. Click **+ Add New MCP Server**.
3. Set:
   * **Name:** `surgical`
   * **Type:** `command`
   * **Command:** `node <FULL_PATH_TO_SURGICAL>/dist/index.js`

---

### B. Claude Code (Terminal CLI)

Run this single command in your terminal:

```bash
# macOS/Linux:
claude mcp add surgical -- node /absolute/path/to/surgical/dist/index.js

# Windows:
claude mcp add surgical -- node "C:\path\to\surgical\dist\index.js"
```

---

### C. GitHub Copilot (VS Code)

Add to your workspace `.vscode/settings.json`:

```json
{
  "github.copilot.chat.mcpServers": {
    "surgical": {
      "command": "node",
      "args": ["<FULL_PATH_TO_SURGICAL>/dist/index.js"]
    }
  }
}
```

---

## 3. Automated Agent Enforcement (Zero Human Friction)

You do not need to run terminal commands manually while coding.

Add the snippet below to your project's `.cursorrules`, `CLAUDE.md`, or `.github/copilot-instructions.md` to force your AI assistant to audit and clean up its own diff before finishing. The same instruction works for Next.js, FastAPI, Go, Rust, and everything else.

```markdown
# Karpathy Rule #3: Surgical Changes Only
Before declaring a task complete, you MUST call the `prune_diff_bloat` MCP tool passing the user's original request as the `intent`.
Generic files (`page.tsx`, `main.py`, `app.py`, `utils.ts`, `lib.rs`) are scored by hunk tokens and identifier overlap—not filenames.
Keep intent-matching and identifier-connected changes. Confirm reversion of isolated drive-by refactors and cosmetic-only diffs (formatter churn).
```

---

## 4. CLI Usage (For Manual Inspection)

You can also run `surgical` directly from the command line outside an editor:

```bash
# Explain what belongs to your intent without touching the working tree
node dist/index.js explain --intent "add navbar to homepage"

# Same scoring on any language (generic app.py / main.py / page.tsx is not discarded)
node dist/index.js explain --intent "add /healthz endpoint"

node dist/index.js explain --intent "fix mobile navbar toggle"

# Plan and non-destructively stage only the matching surgical files
node dist/index.js prune --intent "add navbar to homepage"

# Revert confirmed unrequested drive-by refactors
node dist/index.js prune --intent "add navbar to homepage" --auto-discard
```

| Command | Behavior |
| :--- | :--- |
| `explain --intent <text>` | Prints a dry-run explanation of why each file/hunk is marked surgical vs. bloat. |
| `prune --intent <text>` | Non-destructively runs `git add` on verified surgical files, leaving bloat unstaged. |
| `prune --auto-discard` | Reverts unrequested bloat files after safety snapshotting. |

MCP tools: `prune_diff_bloat`, `explain_diff`, `undo_pruning`.

---

## 5. Safety & Zero Data Loss Guarantee

`surgical` is designed to be **safe by default**:

1. **Automatic Safety Snapshot:** Before any mutation or discard, `surgical` runs `git stash create` / `git write-tree` in `<5ms`. Your entire pre-pruned state is saved as an immutable Git tree object.
2. **Non-Destructive Staging:** By default, `prune` only **stages** the surgical files (`git add`), leaving questionable files unstaged for human review.
3. **Instant Undo:** If a file was pruned that you wanted to keep, run:
   ```bash
   node dist/index.js undo
   ```
   *(or call the MCP tool `undo_pruning`)* to instantly restore your working tree.

---

## 6. Running Tests

The test suite uses a **polyglot** fixture (Python + Go + TypeScript + unrelated SQL + cosmetic CSS) and a FastAPI `/healthz` story (`main.py` + connected `utils/db.py` vs. isolated JWT churn in `routers/auth.py` and Black-only `config.py`):

```bash
npm test
```

---

## Citation

If you reference this tool or the over-editing evaluation methodology, please cite the underlying research:

```bibtex
@article{overediting2026,
  title={When Models Edit Too Much: On the Fidelity of Minimal Code Edits},
  author={Zhu, Tongyao and Lim, Wei Hern and Kan, Min-Yen},
  journal={arXiv preprint arXiv:2609.04061},
  year={2026}
}
```

---

## License

[MIT](LICENSE)