{
  "name": "long-context-refactor-class",
  "description": "Refactor a 200+ line Python class with 8-10 coordinated renames, parameter changes, and docstring updates",
  "dataset": "terminal-bench-local",
  "difficulty": "medium",
  "instruction": "The file inventory_manager.py contains an InventoryManager class that needs refactoring. A refactoring spec is provided in REFACTOR_SPEC.md. You must:\n\n1. Rename methods as specified (old name -> new name)\n2. Update parameter names as specified\n3. Update all docstrings to reflect the new names and parameters\n4. Update all internal call sites within the class where methods call each other\n5. Make sure the class still works correctly after all changes\n\nRead REFACTOR_SPEC.md carefully for the full list of changes. Apply ALL of them. Do not change the overall logic, just the names, parameters, and docstrings as specified. Run python3 test_refactor.py to verify.",
  "setup_files": {
    "REFACTOR_SPEC.md": "# Refactoring Specification for InventoryManager\n\nApply ALL of the following changes:\n\n## Method Renames\n1. `add_item` -> `register_product`\n2. `remove_item` -> `unregister_product`\n3. `get_item` -> `lookup_product`\n4. `update_quantity` -> `adjust_stock`\n5. `get_total_value` -> `calculate_inventory_value`\n6. `apply_discount` -> `set_price_modifier`\n7. `search_items` -> `query_products`\n8. `generate_report` -> `build_inventory_report`\n9. `bulk_add` -> `batch_register`\n10. `export_data` -> `serialize_inventory`\n\n## Parameter Renames (in the renamed methods)\n1. `register_product`: `item_name` -> `product_name`, `item_price` -> `unit_price`, `item_qty` -> `initial_stock`\n2. `unregister_product`: `item_name` -> `product_name`\n3. `lookup_product`: `item_name` -> `product_name`\n4. `adjust_stock`: `item_name` -> `product_name`, `qty_change` -> `stock_delta`\n5. `calculate_inventory_value`: no parameter changes\n6. `set_price_modifier`: `item_name` -> `product_name`, `discount_pct` -> `modifier_pct`\n7. `query_products`: `search_term` -> `query_string`, `min_price` -> `price_floor`, `max_price` -> `price_ceiling`\n8. `build_inventory_report`: `include_empty` -> `include_zero_stock`\n9. `batch_register`: `items_list` -> `product_list`\n10. `serialize_inventory`: `fmt` -> `output_format`\n\n## Docstring Updates\nEvery renamed method must have its docstring updated to use the new method name and new parameter names. Do NOT leave old names in any docstring.\n\n## Internal Call Sites\nSeveral methods call other methods internally. All such calls must be updated to use the new names and parameter names.\n",
    "inventory_manager.py": "class InventoryManager:\n    \"\"\"Manages an inventory of items with prices and quantities.\n    \n    Provides methods for adding, removing, searching, and reporting\n    on inventory items.\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"Initialize an empty inventory.\"\"\"\n        self._inventory = {}\n        self._price_modifiers = {}\n        self._transaction_log = []\n\n    def add_item(self, item_name, item_price, item_qty=0):\n        \"\"\"Add a new item to the inventory.\n        \n        Args:\n            item_name: The name of the item to add.\n            item_price: The unit price of the item.\n            item_qty: Initial quantity in stock (default 0).\n            \n        Returns:\n            True if item was added, False if it already exists.\n            \n        Raises:\n            ValueError: If item_price is negative.\n        \"\"\"\n        if item_price < 0:\n            raise ValueError(f\"Price cannot be negative: {item_price}\")\n        if item_name in self._inventory:\n            return False\n        self._inventory[item_name] = {\n            'name': item_name,\n            'price': item_price,\n            'quantity': item_qty\n        }\n        self._transaction_log.append(f\"ADDED {item_name}: price={item_price}, qty={item_qty}\")\n        return True\n\n    def remove_item(self, item_name):\n        \"\"\"Remove an item from the inventory.\n        \n        Args:\n            item_name: The name of the item to remove.\n            \n        Returns:\n            The removed item dict, or None if not found.\n        \"\"\"\n        item = self._inventory.pop(item_name, None)\n        if item:\n            self._price_modifiers.pop(item_name, None)\n            self._transaction_log.append(f\"REMOVED {item_name}\")\n        return item\n\n    def get_item(self, item_name):\n        \"\"\"Look up an item by name.\n        \n        Args:\n            item_name: The name of the item to look up.\n            \n        Returns:\n            A copy of the item dict, or None if not found.\n        \"\"\"\n        item = self._inventory.get(item_name)\n        if item is None:\n            return None\n        result = dict(item)\n        if item_name in self._price_modifiers:\n            modifier = self._price_modifiers[item_name]\n            result['effective_price'] = result['price'] * (1 - modifier / 100)\n        else:\n            result['effective_price'] = result['price']\n        return result\n\n    def update_quantity(self, item_name, qty_change):\n        \"\"\"Update the quantity of an item in inventory.\n        \n        Args:\n            item_name: The name of the item.\n            qty_change: The amount to add (positive) or subtract (negative).\n            \n        Returns:\n            The new quantity, or None if item not found.\n            \n        Raises:\n            ValueError: If the resulting quantity would be negative.\n        \"\"\"\n        item = self._inventory.get(item_name)\n        if item is None:\n            return None\n        new_qty = item['quantity'] + qty_change\n        if new_qty < 0:\n            raise ValueError(\n                f\"Cannot reduce {item_name} below 0: current={item['quantity']}, change={qty_change}\"\n            )\n        item['quantity'] = new_qty\n        self._transaction_log.append(f\"STOCK {item_name}: change={qty_change}, new_qty={new_qty}\")\n        return new_qty\n\n    def get_total_value(self):\n        \"\"\"Calculate the total value of all inventory.\n        \n        Uses effective prices (after any discounts) multiplied by quantity.\n        \n        Returns:\n            The total inventory value as a float.\n        \"\"\"\n        total = 0.0\n        for item_name, item in self._inventory.items():\n            looked_up = self.get_item(item_name)\n            effective_price = looked_up['effective_price']\n            total += effective_price * item['quantity']\n        return total\n\n    def apply_discount(self, item_name, discount_pct):\n        \"\"\"Apply a percentage discount to an item.\n        \n        Args:\n            item_name: The name of the item.\n            discount_pct: Discount percentage (0-100).\n            \n        Returns:\n            True if discount was applied, False if item not found.\n            \n        Raises:\n            ValueError: If discount_pct is not between 0 and 100.\n        \"\"\"\n        if discount_pct < 0 or discount_pct > 100:\n            raise ValueError(f\"Discount must be 0-100, got {discount_pct}\")\n        if item_name not in self._inventory:\n            return False\n        self._price_modifiers[item_name] = discount_pct\n        self._transaction_log.append(f\"DISCOUNT {item_name}: {discount_pct}%\")\n        return True\n\n    def search_items(self, search_term=None, min_price=None, max_price=None):\n        \"\"\"Search for items matching criteria.\n        \n        Args:\n            search_term: Substring to match in item names (case-insensitive).\n            min_price: Minimum price filter.\n            max_price: Maximum price filter.\n            \n        Returns:\n            A list of item dicts matching the criteria.\n        \"\"\"\n        results = []\n        for item_name in self._inventory:\n            item = self.get_item(item_name)\n            if search_term and search_term.lower() not in item['name'].lower():\n                continue\n            if min_price is not None and item['effective_price'] < min_price:\n                continue\n            if max_price is not None and item['effective_price'] > max_price:\n                continue\n            results.append(item)\n        return results\n\n    def generate_report(self, include_empty=False):\n        \"\"\"Generate an inventory report.\n        \n        Calls get_item to get effective prices and get_total_value for the\n        grand total.\n        \n        Args:\n            include_empty: If True, include items with zero quantity.\n            \n        Returns:\n            A dict with 'items' list and 'total_value'.\n        \"\"\"\n        items = []\n        for item_name in sorted(self._inventory.keys()):\n            item = self.get_item(item_name)\n            if not include_empty and item['quantity'] == 0:\n                continue\n            items.append({\n                'name': item['name'],\n                'price': item['price'],\n                'effective_price': item['effective_price'],\n                'quantity': item['quantity'],\n                'value': item['effective_price'] * item['quantity']\n            })\n        return {\n            'items': items,\n            'total_value': self.get_total_value(),\n            'item_count': len(items)\n        }\n\n    def bulk_add(self, items_list):\n        \"\"\"Add multiple items to inventory at once.\n        \n        Calls add_item for each item in the list.\n        \n        Args:\n            items_list: A list of dicts with keys 'name', 'price', and optionally 'quantity'.\n            \n        Returns:\n            A dict with 'added' count and 'skipped' count.\n        \"\"\"\n        added = 0\n        skipped = 0\n        for item in items_list:\n            qty = item.get('quantity', 0)\n            if self.add_item(item['name'], item['price'], qty):\n                added += 1\n            else:\n                skipped += 1\n        return {'added': added, 'skipped': skipped}\n\n    def export_data(self, fmt='dict'):\n        \"\"\"Export the entire inventory data.\n        \n        Calls get_item for each item to include effective prices.\n        \n        Args:\n            fmt: Output format - 'dict' for a dictionary, 'list' for a list of items,\n                 'summary' for counts only.\n            \n        Returns:\n            The inventory data in the requested format.\n            \n        Raises:\n            ValueError: If fmt is not recognized.\n        \"\"\"\n        if fmt == 'dict':\n            result = {}\n            for item_name in self._inventory:\n                result[item_name] = self.get_item(item_name)\n            return result\n        elif fmt == 'list':\n            return [self.get_item(name) for name in sorted(self._inventory.keys())]\n        elif fmt == 'summary':\n            total_items = len(self._inventory)\n            total_units = sum(i['quantity'] for i in self._inventory.values())\n            return {\n                'total_items': total_items,\n                'total_units': total_units,\n                'total_value': self.get_total_value()\n            }\n        else:\n            raise ValueError(f\"Unknown format: {fmt}\")\n\n    def get_transaction_log(self):\n        \"\"\"Return a copy of the transaction log.\"\"\"\n        return list(self._transaction_log)\n",
    "test_refactor.py": "import sys\nimport ast\nimport inspect\n\n# We will import the module and also parse the AST to check docstrings\n\nerrors = []\nwarnings = []\n\n# --- AST-based checks ---\nwith open('inventory_manager.py', 'r') as f:\n    source = f.read()\n\ntree = ast.parse(source)\n\n# Find the class\nclass_def = None\nfor node in ast.walk(tree):\n    if isinstance(node, ast.ClassDef) and node.name == 'InventoryManager':\n        class_def = node\n        break\n\nif class_def is None:\n    print(\"FAIL: InventoryManager class not found\")\n    sys.exit(1)\n\nmethod_names = []\nfor node in class_def.body:\n    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n        method_names.append(node.name)\n\n# Check old method names are gone\nold_names = [\n    'add_item', 'remove_item', 'get_item', 'update_quantity',\n    'get_total_value', 'apply_discount', 'search_items',\n    'generate_report', 'bulk_add', 'export_data'\n]\nnew_names = [\n    'register_product', 'unregister_product', 'lookup_product', 'adjust_stock',\n    'calculate_inventory_value', 'set_price_modifier', 'query_products',\n    'build_inventory_report', 'batch_register', 'serialize_inventory'\n]\n\nfor old in old_names:\n    if old in method_names:\n        errors.append(f\"Old method name '{old}' still exists\")\n\nfor new in new_names:\n    if new not in method_names:\n        errors.append(f\"New method name '{new}' not found\")\n\n# Check that 'get_transaction_log' and '__init__' are preserved\nif '__init__' not in method_names:\n    errors.append(\"__init__ method is missing\")\nif 'get_transaction_log' not in method_names:\n    errors.append(\"get_transaction_log method is missing\")\n\n# Check parameter names on key methods via AST\ndef get_method_node(name):\n    for node in class_def.body:\n        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:\n            return node\n    return None\n\nparam_checks = {\n    'register_product': ['self', 'product_name', 'unit_price', 'initial_stock'],\n    'unregister_product': ['self', 'product_name'],\n    'lookup_product': ['self', 'product_name'],\n    'adjust_stock': ['self', 'product_name', 'stock_delta'],\n    'set_price_modifier': ['self', 'product_name', 'modifier_pct'],\n    'query_products': ['self', 'query_string', 'price_floor', 'price_ceiling'],\n    'build_inventory_report': ['self', 'include_zero_stock'],\n    'batch_register': ['self', 'product_list'],\n    'serialize_inventory': ['self', 'output_format'],\n}\n\nfor method_name, expected_params in param_checks.items():\n    node = get_method_node(method_name)\n    if node is None:\n        continue  # already reported above\n    actual_params = [arg.arg for arg in node.args.args]\n    for ep in expected_params:\n        if ep not in actual_params:\n            errors.append(f\"{method_name}: expected param '{ep}' not found (has {actual_params})\")\n\n# Check old parameter names are NOT in any docstring\nold_param_names = [\n    'item_name', 'item_price', 'item_qty', 'qty_change',\n    'discount_pct', 'search_term', 'min_price', 'max_price',\n    'include_empty', 'items_list'\n]\n# 'fmt' is too short / generic, skip it\n\nfor node in class_def.body:\n    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n        docstring = ast.get_docstring(node)\n        if docstring and node.name in new_names:\n            for old_param in old_param_names:\n                if old_param in docstring:\n                    errors.append(f\"{node.name} docstring still contains old param name '{old_param}'\")\n\n# Check old method names not referenced in source at all (except comments/strings are ok in test)\nfor old in old_names:\n    # Check if old name appears as a function call in the source\n    if f\"self.{old}(\" in source:\n        errors.append(f\"Internal call site 'self.{old}(' still present in source\")\n\n# --- Runtime checks: import and test basic functionality ---\ntry:\n    # We need to test that the class actually works\n    exec_globals = {}\n    exec(source, exec_globals)\n    IM = exec_globals['InventoryManager']\n    mgr = IM()\n    \n    # Test register_product\n    assert mgr.register_product('Apple', 1.50, 100) == True\n    assert mgr.register_product('Banana', 0.75, 200) == True\n    assert mgr.register_product('Apple', 2.00, 50) == False  # duplicate\n    \n    # Test lookup_product\n    item = mgr.lookup_product('Apple')\n    assert item is not None\n    assert item['name'] == 'Apple'\n    assert item['price'] == 1.50\n    assert item['effective_price'] == 1.50\n    \n    # Test adjust_stock\n    new_qty = mgr.adjust_stock('Apple', -10)\n    assert new_qty == 90\n    \n    # Test set_price_modifier\n    assert mgr.set_price_modifier('Apple', 20) == True\n    item = mgr.lookup_product('Apple')\n    assert abs(item['effective_price'] - 1.20) < 0.01\n    \n    # Test calculate_inventory_value\n    val = mgr.calculate_inventory_value()\n    assert val > 0\n    \n    # Test query_products\n    results = mgr.query_products(query_string='app')\n    assert len(results) == 1\n    results = mgr.query_products(price_floor=1.0)\n    assert len(results) == 1  # Apple (effective 1.20)\n    \n    # Test build_inventory_report\n    mgr.register_product('Cherry', 3.00, 0)\n    report = mgr.build_inventory_report(include_zero_stock=False)\n    assert report['item_count'] == 2  # Apple and Banana (Cherry has 0)\n    report_all = mgr.build_inventory_report(include_zero_stock=True)\n    assert report_all['item_count'] == 3\n    \n    # Test batch_register\n    result = mgr.batch_register([\n        {'name': 'Date', 'price': 5.00, 'quantity': 30},\n        {'name': 'Apple', 'price': 1.50, 'quantity': 10},  # duplicate\n    ])\n    assert result['added'] == 1\n    assert result['skipped'] == 1\n    \n    # Test serialize_inventory\n    data = mgr.serialize_inventory(output_format='list')\n    assert isinstance(data, list)\n    data = mgr.serialize_inventory(output_format='dict')\n    assert isinstance(data, dict)\n    data = mgr.serialize_inventory(output_format='summary')\n    assert 'total_items' in data\n    \n    # Test unregister_product\n    removed = mgr.unregister_product('Cherry')\n    assert removed is not None\n    assert mgr.lookup_product('Cherry') is None\n    \nexcept AssertionError as e:\n    errors.append(f\"Runtime assertion failed: {e}\")\nexcept Exception as e:\n    errors.append(f\"Runtime error: {type(e).__name__}: {e}\")\n\n# --- Report ---\nif errors:\n    print(f\"FAILED with {len(errors)} error(s):\")\n    for e in errors:\n        print(f\"  - {e}\")\n    sys.exit(1)\nelse:\n    print(f\"ALL CHECKS PASSED\")\n    print(f\"  - 10 method renames verified\")\n    print(f\"  - Parameter renames verified\")\n    print(f\"  - Docstrings updated\")\n    print(f\"  - Internal call sites updated\")\n    print(f\"  - Runtime functionality verified\")\n    sys.exit(0)\n"
  },
  "verify": "cd $BENCH_WORK_DIR && python3 test_refactor.py",
  "timeout": 360000,
  "tags": ["long-context", "refactoring", "python", "coordinated-changes"]
}