[
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0235,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0344,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr01",
    "rep": 3,
    "cost_usd": 0.0107,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0279,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Forbidden exception is not defined or imported. Will cause NameError at runtime when a non-admin user calls list_all_orders.",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Incorrect pagination offset calculation. Uses `page * per_page` but should use `(page - 1) * per_page`. For page=1, this produces offset=20 instead of offset=0, causing the first page to skip the first page of records.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0302,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Forbidden exception is not defined or imported. The code raises Forbidden() but it's not imported or defined in the module. This will cause a NameError at runtime. The test expects orders.Forbidden to exist.",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation is incorrect. The offset should be (page - 1) * per_page, not page * per_page. With current code: page=1 produces OFFSET 10 instead of OFFSET 0; page=2 produces OFFSET 40 instead of OFFSET 20. This causes incorrect pagination results.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr02",
    "rep": 3,
    "cost_usd": 0.0249,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Undefined name 'Forbidden'. The exception class is not imported or defined. Will cause NameError at runtime when a non-admin user calls list_all_orders()",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Incorrect pagination offset calculation. Using 'page * per_page' skips records. Should be '(page - 1) * per_page' for standard pagination (page 1 should start at offset 0). Current code makes page=1 return records 21-30 instead of 1-10 when per_page=10",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0148,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 6,
        "severity": "high",
        "issue": "Unsafe access to user['nickname'] will raise KeyError if the key doesn't exist, or AttributeError if the value is None. Code should use user.get('nickname', '').strip() to safely handle missing or null values. Breaks when called with any user record lacking a nickname field or having None as the value.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0703,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "Unsafe access to user[\"nickname\"] without checking if the key exists or if the value is None. Will raise KeyError if nickname key is missing, or AttributeError if nickname is None (cannot call .strip() on None). Should use user.get(\"nickname\", \"\").strip() or similar safe accessor.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr03",
    "rep": 3,
    "cost_usd": 0.0119,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 6,
        "severity": "high",
        "issue": "The code calls `.strip()` on `user[\"nickname\"]` without checking if the key exists or if the value is None. This will raise KeyError if the key is missing, or AttributeError if the value is None (a likely scenario for users who haven't set a nickname). Should use `user.get(\"nickname\")` with fallback handling, e.g., `display_name = (user.get(\"nickname\") or \"\").strip() or user[\"name\"]`",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0415,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: SELECT and UPDATE are separate, non-atomic operations. Another connection could modify the stock between lines 6 and 9, causing inventory to be over-reserved or calculated incorrectly. The original atomic UPDATE statement prevents this.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "high",
        "issue": "TypeError at runtime: Dictionary access row['qty'] assumes dict-like row objects. Standard sqlite3 returns tuples by default; this requires indexing like row[0] or a row_factory configured as sqlite3.Row.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0394,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: Replaced atomic UPDATE with SELECT+UPDATE sequence. If multiple processes reserve stock concurrently, both SELECTs may pass the qty check before either UPDATE executes, allowing overselling. Original atomic UPDATE prevented this by combining check and modification.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "medium",
        "issue": "TypeError with standard sqlite3: Accesses row[\"qty\"] using dict syntax, but fetchone() returns tuples by default unless row_factory is configured. Will crash at runtime unless connection is explicitly configured with sqlite3.Row. Original code avoided this dependency.",
        "label": "fp"
      },
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "Missing rowcount verification: UPDATE statement does not check if any rows were affected. If the stock row is deleted between SELECT (line 6) and UPDATE (line 9), no rows are updated but function still returns True. Original code verified cur.rowcount == 1 to ensure success.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr04",
    "rep": 3,
    "cost_usd": 0.0386,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition introduced by non-atomic operation. Original code used a single UPDATE with WHERE clause (atomic at database level). New code splits this into separate SELECT (line 6) and UPDATE (line 9). Between these operations, another process could modify the stock, causing the UPDATE to operate on stale data. This breaks the atomicity guarantee required by the function and can lead to inventory overselling.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0435,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Calls undefined function orders.get_order_for_user(). This function is not defined in the orders module and will raise AttributeError when the test runs.",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "medium",
        "issue": "The user parameter is received but never used. No authorization check verifies that the user owns the order before allowing the note to be updated. Any user could modify any order's note, unlike list_orders which properly filters by user_id.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0559,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "high",
        "issue": "user parameter is accepted but never used; missing ownership validation allows any authenticated user to update notes on any order",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "NotFound exception raised but not shown as imported in visible diff; will cause NameError if the exception class is not imported",
        "label": "fp"
      },
      {
        "file": "shop/db.py",
        "line": 54,
        "severity": "medium",
        "issue": "SQL query assumes 'note' column exists in orders table; will fail with SQL error if the column has not been created via migration",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr05",
    "rep": 3,
    "cost_usd": 0.062,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "medium",
        "issue": "The `user` parameter is accepted but never used. Missing authorization check to verify the user owns the order before updating the note. Any user can update notes for any order.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0075,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 55,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is interpolated directly into SQL query without parameterization. Use parameterized queries instead: sql = \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\"; return conn.execute(sql, (user_id, f'%{keyword}%')).fetchall()",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.015,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL Injection vulnerability: the 'keyword' parameter is directly embedded in an f-string SQL query without parameterization. An attacker could inject SQL code through the keyword parameter. Use parameterized queries instead: sql = \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\" and pass (user_id, f'%{keyword}%') as parameters.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr06",
    "rep": 3,
    "cost_usd": 0.0301,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: The 'keyword' parameter is directly interpolated into the SQL query using an f-string. An attacker can inject malicious SQL code. Use parameterized queries instead: sql = \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\" and pass (user_id, f'%{keyword}%') to execute()",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0095,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 8,
        "severity": "high",
        "issue": "Hardcoded API key in source code is a critical security vulnerability. API credentials must never be committed to version control. Use os.environ.get() instead.",
        "label": "hit"
      },
      {
        "file": "shop/config.py",
        "line": 7,
        "severity": "medium",
        "issue": "Hardcoded API URL should be externalized to environment variables for proper configuration management across environments.",
        "label": "fp"
      },
      {
        "file": "shop/mailer.py",
        "line": 14,
        "severity": "medium",
        "issue": "urllib.request.urlopen() can raise HTTPError, URLError, and other exceptions that are not caught. Unhandled exceptions will crash the application. Add try/except error handling.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0138,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "Hardcoded API key committed to version control. This credential should be loaded from an environment variable (e.g., os.environ.get('SHOP_MAIL_API_KEY')) to prevent exposure in the repository. Anyone with access to the code can now use this API key.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr07",
    "rep": 3,
    "cost_usd": 0.0317,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "Hardcoded API key committed to repository. API keys must be loaded from environment variables (like SHOP_DB on line 3), never hardcoded in source control.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 14,
        "severity": "medium",
        "issue": "Function signature promises 'int' return type but urllib.request.urlopen() raises urllib.error.HTTPError for non-2xx responses (4xx, 5xx). This violates the contract. Either catch the exception and return the status code, or update the signature to indicate exceptions can be raised.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0558,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0522,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr08",
    "rep": 3,
    "cost_usd": 0.0408,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0112,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Logic regression: Changed from restricting cancellation to only 'pending' orders to allowing cancellation of any order except 'cancelled'. This permits cancelling orders in states like 'shipped', 'processing', or 'delivered', which violates typical order management rules. Orders in these states should not be cancellable.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0329,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 42,
        "severity": "high",
        "issue": "Logic condition changed from 'status != \"pending\"' to 'status == \"cancelled\"'. This breaks the original business requirement that only pending orders can be cancelled. The new code allows any non-cancelled order (including shipped, completed, or processing states) to be cancelled. This violates the intended business logic and could cause inventory/refund errors by cancelling orders that should not be cancellable.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr09",
    "rep": 3,
    "cost_usd": 0.0243,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Logic regression: condition changed from `!= \"pending\"` to `== \"cancelled\"`. This allows cancelling orders with any status except 'cancelled', but the original code only allowed cancelling 'pending' orders. The original error message explicitly states 'only pending orders can be cancelled'. Now orders with status 'shipped', 'completed', 'processing', etc. can be incorrectly cancelled, potentially enabling fraud (receiving items then cancelling for refunds).",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0392,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0438,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "high",
        "issue": "Return type changed from bool to int | None. Existing code that calls reserve_stock() and expects a boolean return will behave differently in conditional statements (e.g., if remaining qty is 0, the boolean check fails even though reservation succeeded).",
        "label": "partial"
      },
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "medium",
        "issue": "fetchone()[0] will raise TypeError if fetchone() returns None. While unlikely in normal transaction context, the row could theoretically be deleted between UPDATE and SELECT. Should check if result is not None before indexing.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr10",
    "rep": 3,
    "cost_usd": 0.0405,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "tests/test_inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Test inserts into 'stock' table without creating it. In-memory database has no schema, so this will fail with 'no such table: stock' error unless db.connect() creates the schema automatically",
        "label": "valid"
      },
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "high",
        "issue": "Race condition: SELECT can read stale data if another thread/connection modifies the stock row between the UPDATE (line 7) and SELECT (line 11), causing the function to return incorrect remaining stock values",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0125,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "round(x) uses Python 3's banker's rounding (round half to even), not 'round half up'. For 12.5, it returns 12 instead of 13, violating the function's contract and name. Original int(x + 0.5) was correct.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation was changed to match incorrect behavior (12 instead of 13). This masks the bug in round_half_up instead of validating it. The test should verify 12.5 rounds UP to 13, not down to 12.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0298,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function `round_half_up` is changed from `int(x + 0.5)` to `round(x)`. The new implementation uses Python's banker's rounding (round to even), not round half up. This breaks the function's contract: `round_half_up(12.5)` was 13, now returns 12. This is a breaking API change.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed from 13 to 12 for `discount_amount(125, 0.10)`. Now tests banker's rounding instead of round half up behavior, contradicting both the test name and the function's intended contract.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr11",
    "rep": 3,
    "cost_usd": 0.0118,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function named `round_half_up` no longer implements 'round half up' semantics. The new `round(x)` uses Python's banker's rounding (round half to even), which produces different results (e.g., 12.5 → 12 instead of 13). This is a breaking change. For example, round_half_up(12.5) previously returned 13, now returns 12. If other code depends on the original behavior, it will silently break.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0069,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Security regression: replaced PBKDF2-HMAC-SHA256 (industry standard password hashing with 200k iterations for key stretching) with MD5, which is cryptographically broken, provides no key stretching, and is unsuitable for password hashing. Enables efficient brute-force and rainbow table attacks.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Breaking change: hash algorithm changed from PBKDF2 to MD5. All existing password hashes will fail verification since verify_password() calls hash_password() with the salt and compares results. This locks out all users with previously stored credentials.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0178,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Security regression: MD5 is cryptographically broken and unsuitable for password hashing. Replacing PBKDF2-HMAC with 200,000 key-stretching iterations with fast MD5 makes brute-force attacks computationally feasible.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Authentication breakage: Existing passwords hashed with PBKDF2 cannot be verified using MD5. All existing users will fail password verification.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr12",
    "rep": 3,
    "cost_usd": 0.0151,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 14,
        "severity": "high",
        "issue": "Critical security regression: replacing PBKDF2-HMAC-SHA256 with MD5 for password hashing. MD5 is cryptographically broken and unsuitable for password storage. This makes user credentials vulnerable to efficient cracking attacks and significantly weakens security. PBKDF2 with 200,000 iterations should be retained or upgraded to bcrypt/argon2.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0103,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0303,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr13",
    "rep": 3,
    "cost_usd": 0.0269,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.03,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0406,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr14",
    "rep": 3,
    "cost_usd": 0.0466,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "medium",
        "issue": "Test creates orders but doesn't explicitly commit before assertion. If `create_order` doesn't auto-commit, the count query may not see the inserted rows depending on transaction isolation. Compare with `test_cancel_restores_stock` which has a commit (via `cancel_order`) before its assertion. Add `conn.commit()` after line 40 for consistency and reliability.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0348,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0424,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "A",
    "pr": "pr15",
    "rep": 3,
    "cost_usd": 0.0337,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0077,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 45,
        "severity": "high",
        "issue": "Boundary condition is incorrect. PR description states '小計が10000セント以上なら無料' (subtotal >= 10000 is free), but code checks `if subtotal > 10000` (strictly greater). When subtotal equals exactly 10000, it should return 0 but returns 1200. Should be `if subtotal >= 10000:`",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 25,
        "severity": "medium",
        "issue": "Test does not cover the boundary case at subtotal == 10000. Should test both 9999 and 10000 to catch the off-by-one error in the implementation",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0092,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "high",
        "issue": "Boundary condition mismatch: The PR description states '小計が10000セント以上なら無料' (free if subtotal is 10000 or more), but the code uses `if subtotal > 10000:`. This means subtotal=10000 will incorrectly charge 1200 instead of being free. Should be `if subtotal >= 10000:`",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 24,
        "severity": "high",
        "issue": "Missing boundary test case: The test suite does not cover the critical boundary at 10000 (e.g., `assert pricing.express_shipping_fee(10000) == 0`), which would expose the off-by-one error in the condition",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr01",
    "rep": 3,
    "cost_usd": 0.0128,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Boundary condition is incorrect. The condition should be `>= 10000` not `> 10000`. According to the PR description, a subtotal of exactly 10000 should be free (10000セント以上なら無料), but the current code will charge 1200 for it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.054,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation is incorrect. Should be (page - 1) * per_page instead of page * per_page, since pages are 1-indexed per the PR description (\"page は1始まり\"). Current code causes page=1 to return items starting at offset 20 instead of offset 0, skipping the first page of results.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.069,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Undefined exception 'Forbidden' - used in raise statement but not imported or defined in the module. The test expects 'orders.Forbidden' to exist, which will cause NameError at runtime when a non-admin user calls list_all_orders",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 52,
        "severity": "high",
        "issue": "Pagination offset calculation bug: uses 'page * per_page' instead of '(page - 1) * per_page'. For page=1 with per_page=20, this generates offset=20 (skipping the first 20 items) instead of offset=0. All pages will be off by one offset",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr02",
    "rep": 3,
    "cost_usd": 0.072,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Undefined exception class 'Forbidden'. This name is not defined or imported in the module. When a non-admin user calls the function, this will raise NameError instead of the intended Forbidden exception. Either define Forbidden in this module or add an import statement (e.g., from werkzeug.exceptions import Forbidden, or define a custom Forbidden exception class).",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Incorrect pagination offset calculation. The formula 'page * per_page' is wrong for 1-indexed pagination. For page=1, this yields offset=per_page (skipping the first page of results), when it should be 0. Correct formula: offset = (page - 1) * per_page. This causes page 1 to return results from page 2, page 2 to return results from page 3, etc.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "high",
        "issue": "Reference to undefined 'orders.Forbidden' exception class. The test will fail with AttributeError when pytest tries to evaluate this class reference, since Forbidden is not defined in the orders module.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0196,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 6,
        "severity": "high",
        "issue": "The code assumes user[\"nickname\"] exists and is a string. If the user object lacks this key (e.g., from existing database records predating this schema change) or if the value is None, this will raise KeyError or AttributeError at runtime. Should use user.get(\"nickname\") or similar defensive access pattern.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0445,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "KeyError if user dict doesn't have 'nickname' key; AttributeError if nickname is None. Should use user.get('nickname', '') instead, e.g.: display_name = (user.get('nickname', '') or '').strip() or user['name']",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr03",
    "rep": 3,
    "cost_usd": 0.0234,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 6,
        "severity": "high",
        "issue": "Code accesses user[\"nickname\"] without verifying the field exists in the dictionary or handling None values. Will raise KeyError if nickname key missing or AttributeError if value is None. The PR contains no schema changes or db.get_user() updates to guarantee this field exists and is a string.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0104,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: Changed from atomic UPDATE statement to non-atomic SELECT + UPDATE. Between SELECT and UPDATE, another process could modify the stock, causing double-reservation or lost updates. Example: if two processes read qty=10 concurrently, both will proceed and create qty=5 twice instead of sequential deductions. Original atomic approach was correct; PR description claims 'behavior unchanged' but this is false under concurrency.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0314,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition introduced: The original atomic SQL UPDATE (check qty and update in one operation) is refactored into separate SELECT then UPDATE. Concurrent transactions can both read sufficient stock and both proceed to update, causing inventory to go negative or be oversold. Example: two concurrent requests each see qty=3, both think they can reserve 3, both update, result is qty=0 but 6 items were reserved.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "high",
        "issue": "Dictionary-like row access row[\"qty\"] requires the database connection's row_factory to support dict-like access (e.g., sqlite3.Row). The original code never accesses row fields, so this configuration is not evident from the diff. Default sqlite3.Row() returns tuples which don't support string indexing, causing TypeError at runtime.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr04",
    "rep": 3,
    "cost_usd": 0.0331,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: The original atomic UPDATE statement has been replaced with separate SELECT (line 6) and UPDATE (line 9) operations. Between these statements, another process could modify stock, violating the atomicity guarantee and causing incorrect inventory or failed reserve operations. The atomic UPDATE...WHERE approach must be preserved for data integrity.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "medium",
        "issue": "Dictionary-style row access assumes database configuration: Accessing row['qty'] requires the database connection to support Row objects with dictionary-style access (e.g., conn.row_factory = sqlite3.Row). Without this configuration, a TypeError will occur at runtime.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0693,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing authorization check: The `update_order_note` function takes a `user` parameter but never verifies that the user owns the order before updating it, allowing any user to modify any order's note",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "The SQL query assumes a 'note' column exists in the 'orders' table, but no database migration is included in this PR. This will cause a runtime SQL error if the column doesn't exist",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0398,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Order ownership is not verified. The user parameter is passed to the function but never used to validate that the order belongs to the user. This allows any user to update any order's note, which is a security issue.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr05",
    "rep": 3,
    "cost_usd": 0.0577,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Parameter 'user' is declared but never used to authorize that the user owns the order. Any authenticated user can modify notes on any order. Should validate that order belongs to the user before allowing the update.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "medium",
        "issue": "Calls orders.get_order_for_user() which is not defined in the PR changes. Test will fail with AttributeError if this function doesn't exist in the codebase.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0083,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 56,
        "severity": "high",
        "issue": "SQL injection vulnerability: The `keyword` parameter is interpolated directly into the SQL string using an f-string. An attacker can inject arbitrary SQL by passing a crafted keyword like `%' OR '1'='1` or `'; DROP TABLE orders; --`. Must use parameterized queries with placeholders instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0319,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: The keyword parameter is directly interpolated into the SQL query using an f-string without parameterization. An attacker could inject arbitrary SQL commands by providing payloads like `test' OR '1'='1` or UNION-based attacks. Use parameterized queries with ? placeholders: sql = \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\"; return conn.execute(sql, (user_id, f'%{keyword}%')).fetchall()",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr06",
    "rep": 3,
    "cost_usd": 0.0221,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: the 'keyword' parameter is directly interpolated into the SQL string without escaping or parameterization. Attackers could execute arbitrary SQL by passing input like \"' OR '1'='1\" or \"\"; DROP TABLE orders; --\". Use parameterized queries: sql = \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\"; return conn.execute(sql, (user_id, f\"%{keyword}%\")).fetchall()",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0104,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 7,
        "severity": "high",
        "issue": "Hardcoded API key should never be committed to source code. Use os.environ.get(\"MAIL_API_KEY\") to read from environment variables instead, following the pattern used for DB_PATH. This secret will be visible in git history and accessible to anyone with repository access.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0945,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "Hardcoded API key exposed in source code. API credentials must never be committed to version control. Should use `os.environ.get('SHOP_MAIL_API_KEY')` pattern consistent with DB_PATH on line 3.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr07",
    "rep": 3,
    "cost_usd": 0.01,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 7,
        "severity": "high",
        "issue": "Hardcoded API key exposed in repository. The value 'sk_live_EXAMPLE_ONLY_NOT_A_REAL_KEY' appears to be a real secret key (Stripe-like format). Must use os.environ.get() like MAIL_API_URL does on line 6. Secrets in version control can be compromised.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 14,
        "severity": "high",
        "issue": "Unhandled exceptions from urllib.request.urlopen(). Will raise HTTPError, URLError, or socket.timeout if the mail service is unreachable or returns an error status. Should wrap in try-except or document that caller must handle these exceptions.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0452,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0805,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr08",
    "rep": 3,
    "cost_usd": 0.0462,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.012,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 40,
        "severity": "high",
        "issue": "Logic error: Changed from checking `!= \"pending\"` to checking `== \"cancelled\"`. This allows cancellation of shipped/delivered orders, breaking the requirement that only pending orders can be cancelled. The PR description states this specification is unchanged, but the implementation contradicts that. Should check both conditions: if status is cancelled (already cancelled) OR if status is not pending (cannot cancel non-pending orders).",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.008,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 39,
        "severity": "high",
        "issue": "Logic change breaks the requirement that only pending orders can be cancelled. Original code rejected any non-pending status; new code only rejects 'cancelled' status, allowing 'shipped' and 'delivered' orders to be cancelled. PR description states this requirement is unchanged, but the code change violates it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr09",
    "rep": 3,
    "cost_usd": 0.0096,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 39,
        "severity": "high",
        "issue": "Logic change breaks specification. Original code enforces 'only pending orders can be cancelled' by checking `!= pending`. New code only rejects already-cancelled orders, allowing cancellation of completed/shipped orders. This violates the stated requirement: 'キャンセルできるのは pending の注文だけ、という仕様は変えていません' (specification that only pending orders can be cancelled unchanged). Will cause incorrect stock release and data corruption.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0424,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "high",
        "issue": "Race condition between UPDATE and SELECT: the SELECT statement on line 11 executes separately after the UPDATE on line 7-8. Another connection could modify or delete the stock row between these operations, causing an incorrect remaining stock value to be returned. If the row is deleted, fetchone()[0] will crash with TypeError.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0345,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "medium",
        "issue": "Race condition: The UPDATE (line 6-8) and SELECT (line 11) operations are not atomic. Between the UPDATE (which deducts stock) and SELECT (which reads remaining), another connection could modify the stock value, causing the returned remaining count to be inaccurate or stale.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr10",
    "rep": 3,
    "cost_usd": 0.0509,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "high",
        "issue": "Unhandled None: fetchone() can return None if no rows are found. Calling [0] on None causes TypeError. Must check if result is not None before indexing.",
        "label": "fp"
      },
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "medium",
        "issue": "Race condition: SELECT at line 11 is not atomic with UPDATE at lines 6-8. Another transaction could modify the stock between these queries, causing an incorrect remaining count to be returned.",
        "label": "fp"
      },
      {
        "file": "tests/test_inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Test will fail: 'stock' table does not exist. The INSERT statement requires the table to be created first, e.g., c.execute('CREATE TABLE stock (sku TEXT PRIMARY KEY, qty INTEGER)').",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0285,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function `round_half_up` now implements Python's banker's rounding (round to nearest even) via `round()`, but the name suggests round-half-up semantics (rounding 0.5 values away from zero). This mismatch is misleading and will break any code that depends on the function actually doing round-half-up. For x=12.5, this now returns 12 instead of 13. In a pricing system, this changes discount calculations. Either rename the function to match its actual behavior or change the implementation to match the name.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.02,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function 'round_half_up' no longer implements round-half-up behavior. Python's round() uses banker's rounding (round-half-to-even), not round-half-up. This changes results for .5 values: 12.5 now rounds to 12 instead of 13. In a pricing/discount system, this breaks the function's contract (indicated by its name) and silently changes discount calculations. Either rename the function or revert to int(x + 0.5) to maintain round-half-up semantics.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr11",
    "rep": 3,
    "cost_usd": 0.0529,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Function 'round_half_up' now uses Python's round() which implements banker's rounding (round-half-to-even), not round-half-up. For 12.5 this returns 12 instead of 13. This breaks the function contract implied by its name and could break pricing logic in code depending on true round-half-up behavior.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed to accept banker's rounding (12.5 → 12) without renaming the function or documenting the semantic change. Function 'round_half_up' is now semantically incorrect. Rename function or restore true round-half-up implementation.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0077,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "MD5 is cryptographically broken and unsuitable for password hashing. It is fast to compute, enabling brute-force attacks, and has known collision vulnerabilities. PBKDF2-SHA256 with 200,000 iterations is a proper key derivation function designed to resist such attacks. This change creates a critical security vulnerability that exposes all user passwords to compromise if the database is breached. If login performance is a concern, optimize other system components rather than weaken password security, or upgrade to modern schemes like bcrypt or Argon2.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0068,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 11,
        "severity": "high",
        "issue": "MD5 is cryptographically broken and unsuitable for password hashing. Replacing PBKDF2-HMAC-SHA256 (with 200k iterations) with MD5(salt+password) severely weakens password security. If the password database is compromised, MD5 hashes can be cracked trivially using modern hardware and rainbow tables. The slowness of PBKDF2 is intentional—it makes brute force attacks expensive. Use bcrypt, scrypt, argon2, or keep PBKDF2 with appropriate iteration counts.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr12",
    "rep": 3,
    "cost_usd": 0.0061,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "MD5 is cryptographically broken and unsuitable for password hashing. It is fast to compute, enabling efficient brute-force attacks. PBKDF2 with 200,000 iterations provides proper key stretching to make attacks expensive. This change introduces a critical security vulnerability that could lead to mass user account compromise if the password database is breached. Use bcrypt, scrypt, or argon2 instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0109,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0162,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr13",
    "rep": 3,
    "cost_usd": 0.0104,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0536,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0302,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr14",
    "rep": 3,
    "cost_usd": 0.051,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0431,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0286,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "B",
    "pr": "pr15",
    "rep": 3,
    "cost_usd": 0.0234,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0347,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Condition uses '>' instead of '>=' - contradicts PR description which states '10000セント以上' (10000 yen or more). At subtotal=10000, should return 0 but returns 1200",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 26,
        "severity": "high",
        "issue": "Test missing critical boundary case at subtotal=10000. Only tests 9999 and 20000, should also verify the exact threshold",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0164,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Condition uses '> 10000' but PR description states '≥ 10000'. Subtotal of exactly 10000 cents will incorrectly return 1200 instead of 0",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr01",
    "rep": 3,
    "cost_usd": 0.0464,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Free shipping threshold is 10000, but SPEC.md requires 5000 cents. Also uses '>' instead of '>=' per spec's '以上' (or more)",
        "label": "hit"
      },
      {
        "file": "shop/pricing.py",
        "line": 46,
        "severity": "high",
        "issue": "Shipping fee is 1200 cents, but SPEC.md Section 1 specifies 500 cents for amounts below threshold",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0379,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset uses 'page * per_page' instead of '(page - 1) * per_page', violating 1-based pagination spec",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0581,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation is wrong. Uses 'page * per_page' but should use '(page - 1) * per_page' for 1-based page numbering. This causes page 1 to skip the first per_page items and return rows 11-20 instead of 1-10, violating the pagination requirement.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr02",
    "rep": 3,
    "cost_usd": 0.0244,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Incorrect pagination offset calculation: uses `page * per_page` instead of `(page - 1) * per_page`. With page=1, this skips the first 20 records instead of showing them. Inconsistent with list_my_orders on line 36 and violates pagination requirement.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0359,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": ".strip() on NULL nickname raises AttributeError; database allows NULL nicknames, fixtures include bob with NULL",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0313,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "AttributeError when user['nickname'] is NULL from database. Calling .strip() on None will crash. Code should use (user['nickname'] or '').strip() to handle nullable nickname field.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr03",
    "rep": 3,
    "cost_usd": 0.0299,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "Code crashes with AttributeError when user['nickname'] is NULL. The database schema allows NULL nicknames, but the code calls .strip() on None without null-checking first.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0628,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "Race condition in SELECT-CHECK-UPDATE pattern: concurrent orders can cause lost updates where later updates overwrite earlier ones, violating the requirement that concurrent orders must not cause negative inventory",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0792,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: SELECT (line 6) and UPDATE (line 9) are not atomic. With concurrent orders, both transactions can read the same qty value before either updates, causing inventory to be double-reserved. This violates the requirement that inventory must not be over-committed with concurrent orders (requirement 2).",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr04",
    "rep": 3,
    "cost_usd": 0.0536,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "Race condition between SELECT (line 6) and UPDATE (line 9). Concurrent transactions can both read the same qty value and both update based on stale data, violating requirement '同時注文で在庫がマイナスになってはならない。' The atomic UPDATE statement in the original code prevented this.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0424,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Missing authorization check. Should use get_order_for_user(conn, user, order_id) instead of db.get_order(conn, order_id) to verify user owns the order or is admin, as required by SPEC.md Section 3.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.019,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Authorization check missing: function should use get_order_for_user() instead of db.get_order() to enforce that only the order owner or admins can update notes. Violates SPEC.md Section 3 authorization requirement.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr05",
    "rep": 3,
    "cost_usd": 0.0232,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing authorization check: should use get_order_for_user() instead of db.get_order() to verify the user owns the order (or is admin), as required by SPEC.md section 3. Currently any authenticated user can update notes on any order.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.043,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is interpolated using f-string instead of parameterized query. Violates requirement: 'すべてのSQLはパラメータ化する。'",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0235,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: SQL query constructed with f-string interpolation violates SPEC.md requirement 4 that 'all SQL must be parameterized'. The keyword parameter should be passed as a parameter, not interpolated into the query string.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr06",
    "rep": 3,
    "cost_usd": 0.0159,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is not parameterized, violates requirement 'すべてのSQLはパラメータ化する。' Use parameterized query with ? placeholders",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.024,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "MAIL_API_URL is hardcoded. Requirement 3 (SPEC.md) states: '秘密情報(APIキー等)はリポジトリに含めず環境変数から読む' (Secret information should not be in the repository but read from environment variables). Should use os.environ.get()",
        "label": "fp"
      },
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "MAIL_API_KEY is hardcoded in the repository. This violates Requirement 3 (SPEC.md line 18) which explicitly states secret information like API keys must not be in the repository. This is a security issue - the key is now in version history. Should use os.environ.get()",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0198,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "Hardcoded API key violates SPEC.md requirement 3: secret information must be read from environment variables, not stored in repository",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr07",
    "rep": 3,
    "cost_usd": 0.0295,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "Hardcoded API key violates SPEC.md requirement 3: secret information must be read from environment variables, not stored in repository. This exposes sensitive credentials to anyone with repository access.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0406,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "high",
        "issue": "Function violates spec requirement: coupon and bulk discount cannot be used together. Implementation applies both (lines 48-49); should apply only the discount with larger amount.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0577,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "high",
        "issue": "Applies both bulk and coupon discounts together; spec requires only the larger discount should apply",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr08",
    "rep": 3,
    "cost_usd": 0.0495,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "high",
        "issue": "Function applies both bulk discount and coupon discount, violating SPEC.md requirement that they are mutually exclusive - only the larger discount should apply",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0297,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Allows cancellation of non-pending orders; violates spec requirement that only pending orders can be cancelled",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0371,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Violates SPEC.md requirement #4: allows cancelling non-'pending' orders; specification requires only 'pending' orders can be cancelled",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr09",
    "rep": 3,
    "cost_usd": 0.026,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Violates SPEC requirement: only orders with status 'pending' should be cancellable. New code allows cancelling non-pending orders (e.g., 'shipped') as long as they're not already 'cancelled'",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.061,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "Change to reserve_stock return type (int|None instead of bool) breaks this check. When remaining inventory is exactly 0 (a successful reservation), 'if not 0:' incorrectly raises OutOfStock. Should be 'if inventory.reserve_stock(...) is None:' to properly check for failure.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0409,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "Logic error: using 'if not' on the new int|None return type will incorrectly raise OutOfStock when remaining stock is 0. Should use 'if ... is None:' instead",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr10",
    "rep": 3,
    "cost_usd": 0.0425,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "The truthiness check fails when reserve_stock returns 0 (successful reservation leaving zero stock). The return type changed from bool to int|None, so this code now incorrectly raises OutOfStock when the last item is reserved.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0258,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Changed to use Python's round() which uses banker's rounding (round-half-to-even), but specification requires half-up rounding where 0.5 always rounds up. For example, round(12.5) returns 12 instead of 13.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed from 13 to 12 to match the incorrect implementation instead of the specification. The specification explicitly requires half-up rounding where 12.5 should round to 13.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0241,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Changing from int(x + 0.5) to round(x) violates the requirement for 'half-up' rounding. Python's round() uses banker's rounding (round half to even), not half-up. For example, round(12.5) = 12, but the spec requires 12.5 → 13",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expected value changed from 13 to 12 to match incorrect implementation. Should revert the implementation instead, not the test",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr11",
    "rep": 3,
    "cost_usd": 0.0245,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Implementation uses Python's round() which applies banker's rounding (round half to even), not the specification requirement of round half up. For example, 12.5 rounds to 12 instead of 13, violating SPEC.md section 1 requirement that discount amounts use 'half up' rounding.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed to match incorrect rounding behavior. The test expects 12.5 to round to 12, but SPEC.md requires 0.5 to round up to 13. This test now validates incorrect behavior.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0299,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Requirement violation and critical security issue: SPEC.md Section 3 requires PBKDF2-SHA256 for password hashing, but this change uses MD5 instead. MD5 is cryptographically broken and unsuitable for password hashing, making passwords vulnerable to brute force attacks.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0118,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "SPEC.md section 3 requires 'パスワードは salt 付き PBKDF2-SHA256 で保存する' (passwords must be stored with salt using PBKDF2-SHA256), but this change uses MD5 instead. MD5 is cryptographically broken and unsuitable for password hashing. PBKDF2 with 200,000 iterations was intentionally slow to resist brute-force attacks; MD5 defeats this security measure entirely.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr12",
    "rep": 3,
    "cost_usd": 0.018,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Violates SPEC.md requirement 3 which mandates 'PBKDF2-SHA256' for password hashing. Changing to MD5 is also a critical security vulnerability: MD5 is cryptographically broken and too fast for password hashing. Fast hashing enables brute-force attacks. Must use PBKDF2-SHA256 or modern alternatives like bcrypt/Argon2.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0532,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0792,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr13",
    "rep": 3,
    "cost_usd": 0.0508,
    "turns": 14,
    "tool_calls": 13,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.1508,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "high",
        "issue": "count_my_orders() missing admin privilege check; should count all orders for admins per SPEC.md Section 3",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.1067,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr14",
    "rep": 3,
    "cost_usd": 0.1341,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0804,
    "turns": 14,
    "tool_calls": 13,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0482,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "C",
    "pr": "pr15",
    "rep": 3,
    "cost_usd": 0.0699,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0363,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Boundary condition is > 10000 instead of >= 10000. When subtotal is exactly 10000, code returns 1200 but spec requires 0 (free). Violates PR requirement: 小計が10000セント以上なら無料",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 26,
        "severity": "high",
        "issue": "Missing boundary test at exactly 10000 (the threshold). Only tests 9999 and 20000. REVIEW.md requires testing spec boundary values directly. Should include: assert pricing.express_shipping_fee(10000) == 0",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0351,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Boundary condition incorrect: code uses `> 10000` but requirement states `以上10000` (>= 10000). When subtotal == 10000, function incorrectly returns 1200 instead of 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr01",
    "rep": 3,
    "cost_usd": 0.052,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Boundary condition uses > instead of >=. PR spec states '10000セント以上' (>= 10000) but code implements > 10000. At exactly 10000 yen, function incorrectly returns 1200 instead of 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0368,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset is calculated as `page * per_page` instead of `(page - 1) * per_page`. This causes page=1 to skip the first per_page items, returning wrong data. Inconsistent with list_my_orders which correctly uses (page - 1) * per_page.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0404,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Offset calculation uses `page * per_page` instead of `(page - 1) * per_page` for 1-indexed pages, returning wrong page data",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr02",
    "rep": 3,
    "cost_usd": 0.0415,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Incorrect offset calculation: uses `page * per_page` instead of `(page - 1) * per_page`. For 1-indexed pages, this returns wrong items (e.g., page=1 skips first 10 items instead of returning them). Inconsistent with existing `list_my_orders` function and breaks pagination spec.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 44,
        "severity": "medium",
        "issue": "Test assertion `assert 0 < len(page1) <= 10` only validates length, not correctness of returned items. Weak test that doesn't catch the pagination offset bug. Should verify actual order IDs or test multiple pages return different items.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0656,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": ".strip() on user[\"nickname\"] which can be None (NULL in database), causing AttributeError for users without nickname (like bob)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0489,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "Calls .strip() on user[\"nickname\"] without null check; causes AttributeError when nickname is NULL",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr03",
    "rep": 3,
    "cost_usd": 0.075,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "AttributeError when nickname is NULL. The code calls user[\"nickname\"].strip() but the schema allows NULL nicknames (see db.py line 8 and test fixture where Bob has nickname=NULL). Should use (user[\"nickname\"] or \"\").strip() to handle NULL values.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "medium",
        "issue": "Test only verifies nickname is used when it exists (Alice case). Per REVIEW.md requirement to test boundary values, it must also verify fallback behavior when nickname is NULL or empty string. The fixture already has Bob with NULL nickname but the test doesn't verify this fallback case.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0469,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "UPDATE statement lacks the WHERE condition to make stock reservation atomic. This creates a race condition between SELECT (line 6) and UPDATE (line 9): concurrent requests can both read the same qty, both pass the check, and both reduce from the same value. Violates REVIEW.md requirement for conditional UPDATE in single transaction (read-modify-write prohibited) and SPEC.md requirement that concurrent orders cannot cause overbooking.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0645,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: read-modify-write pattern violates REVIEW.md atomic UPDATE requirement and SPEC.md concurrent order requirement. Concurrent transactions can both read same qty, both pass check, and both update, resulting in incorrect inventory.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr04",
    "rep": 3,
    "cost_usd": 0.0497,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Violates REVIEW.md requirement (line 8): 'read-modify-write禁止' (read-modify-write forbidden). Changed from atomic conditional UPDATE to non-atomic SELECT-then-UPDATE pattern. Multiple concurrent transactions could read the same qty and both succeed, causing inventory inconsistencies.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0351,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing authorization check. The function should use `get_order_for_user(conn, user, order_id)` instead of `db.get_order(conn, order_id)` to verify the user owns the order or is an admin. Currently any user can update notes on any order, violating REVIEW.md's requirement: '認可チェックは shop/orders.py の関数の入口で行う。ユーザーID由来のデータを返す関数は所有者を確認する。' This is evident by comparing to `cancel_order` which correctly uses `get_order_for_user`.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0902,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing authorization check. The function should call get_order_for_user(conn, user, order_id) instead of db.get_order(conn, order_id) to verify the user owns the order before allowing modification. Currently any user can update any order's note.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr05",
    "rep": 3,
    "cost_usd": 0.031,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing authorization check. The function calls db.get_order() instead of get_order_for_user(), allowing any user to modify notes on orders they don't own. The `user` parameter is passed but never checked. Should use get_order_for_user() like cancel_order() does at line 40.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0398,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is directly interpolated into SQL string instead of being parameterized. Violates SPEC.md requirement 4 and REVIEW.md requirement that all SQL must use placeholders (?). Attacker-controlled keyword could break query logic or expose unintended data.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0347,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: uses string interpolation instead of parameterized queries. Both user_id and keyword are directly embedded in SQL string. Violates SPEC.md Section 4 requirement 'すべてのSQLはパラメータ化する' and REVIEW.md requirement 'SQLは必ずプレースホルダ(?)でパラメータ化'. When keyword contains SQL metacharacters or commands, arbitrary SQL can be executed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr06",
    "rep": 3,
    "cost_usd": 0.0326,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is interpolated with f-string instead of parameterized with ?. Violates SPEC.md section 4 and REVIEW.md requirement. Attacker can inject SQL to bypass authorization or manipulate query logic.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.1247,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "MAIL_API_KEY is hardcoded as a literal string instead of being read from environment variable. Violates SPEC.md section 3 (秘密情報はリポジトリに含めず環境変数から読む) and REVIEW.md (秘密情報・鍵・トークンのコミット禁止). API keys and other secrets must not be committed to the repository.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.017,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "Hardcoded API key violates SPEC requirement 3: 秘密情報(APIキー等)はリポジトリに含めず環境変数から読む。 This sensitive credential must be read from environment variable (os.environ.get) instead of being committed to the repository.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr07",
    "rep": 3,
    "cost_usd": 0.0459,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "MAIL_API_KEY is hardcoded with a real API key (sk_live_...). SPEC.md and REVIEW.md explicitly forbid committing secret information/keys to the repository. Must use os.environ.get(\"MAIL_API_KEY\") instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.1603,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "high",
        "issue": "Function applies both bulk discount AND coupon, violating SPEC.md requirement that they cannot be combined. Per spec, when both apply, only the larger discount amount should be used. Current code calculates subtotal with bulk discount (line 48), then subtracts coupon (line 49), applying both. Should compare both discount amounts and apply only the larger one.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 25,
        "severity": "high",
        "issue": "Test only covers qty=2 (no bulk discount applies). Per SPEC.md and REVIEW.md, tests must verify boundary values. Cannot verify the critical requirement that bulk discount and coupon are mutually exclusive. Should add test case with qty≥10 to verify the interaction when bulk discount would apply.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0874,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 48,
        "severity": "high",
        "issue": "Applies both bulk discount and coupon discount together, violating SPEC.md requirement to apply only the larger discount",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr08",
    "rep": 3,
    "cost_usd": 0.0974,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "Function applies both bulk discount (line 48) and coupon discount (line 49) to the same order. SPEC.md requires these are mutually exclusive: 'クーポンはまとめ買い割引と併用できない。両方該当する場合は割引額が大きい方だけを適用する。' Only the discount type producing larger savings should be applied, not both.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.041,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Allows cancelling non-pending orders, violating SPEC.md Section 4 requirement",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0471,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Violates SPEC.md Section 4: allows cancellation of any non-cancelled order instead of only status='pending' orders",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr09",
    "rep": 3,
    "cost_usd": 0.0343,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Specification violation: condition changed from `!= \"pending\"` to `== \"cancelled\"` allows cancelling non-pending orders (completed, shipped, etc.), violating SPEC.md Section 4 requirement that only pending orders can be cancelled",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.121,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "Caller not updated for reserve_stock return type change from bool to int|None. The condition 'if not inventory.reserve_stock(...)' will incorrectly treat remaining stock of 0 as OutOfStock failure when reservation actually succeeded.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "medium",
        "issue": "Violates REVIEW.md: separate SELECT after UPDATE breaks the 'read-modify-write禁止' principle. Stock quantity should be obtained atomically with the update operation (e.g., using SQLite RETURNING clause) not in a separate query.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0289,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "Caller not updated for new return type. When reserve_stock returns 0 (zero remaining items), 'if not 0:' is True and incorrectly raises OutOfStock despite successful reservation. Only None should raise OutOfStock.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 10,
        "severity": "high",
        "issue": "Read-modify-write anti-pattern violates REVIEW.md: UPDATE and SELECT are separate, allowing concurrent transactions to modify stock between them. Must use 'UPDATE...RETURNING qty' to ensure atomicity.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr10",
    "rep": 3,
    "cost_usd": 0.0639,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "reserve_stock return type changed from bool to int|None, but this call site still uses truthiness check. When remaining stock is 0 (falsy), it incorrectly raises OutOfStock despite successful reservation. Must change to 'is None' check.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0403,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Implementation violates SPEC.md requirement for 'half up' rounding. Python's round() uses banker's rounding (round half to even), not half up. This will cause incorrect discount amounts when the fractional part is exactly 0.5 and would round to an even number. Example: 12.5 rounds to 12 instead of 13.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed to match wrong implementation instead of verifying SPEC.md requirement. The test case discount_amount(125, 0.10)=12.5 should round UP to 13 per spec 'half up' requirement, not down to 12. This violates REVIEW.md convention that tests must verify spec boundary values directly.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0245,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Replacing int(x + 0.5) with round(x) violates SPEC.md requirement for half-up rounding. Python's round() uses banker's rounding (round-to-even), which produces incorrect results at .5 boundaries (e.g., 12.5→12 instead of 12.5→13). This breaks discount calculations required by specification.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed to match incorrect implementation behavior instead of verifying specification. REVIEW.md forbids this pattern. The correct half-up rounding of 12.5 is 13, not 12. This masks a specification violation.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr11",
    "rep": 3,
    "cost_usd": 0.05,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Python's round() uses banker's rounding (round half to even), not half-up rounding. For 12.5, it returns 12 instead of 13. Specification requires '0.5は切り上げ = half up' for discount calculations. The original int(x + 0.5) correctly implemented this requirement.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation is incorrect. discount_amount(125, 0.10) = 12.5 should round to 13 (half-up) per SPEC.md section 1, not 12. The test should verify the specification requirement, not just match the implementation output.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0311,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "SPEC.md requires PBKDF2-SHA256 for password hashing (Section 3: 認証・認可), but this changes it to MD5, which is cryptographically broken and unsuitable for password storage. This violates the security specification and weakens password protection.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0242,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Password hashing changed from PBKDF2-SHA256 to MD5, violating SPEC.md section 3 requirement and introducing critical security vulnerability. MD5 is unsuitable for password hashing and vulnerable to brute force attacks; the 200k iterations of PBKDF2 intentionally slow attacks. This breaks password security when any attacker gains hash access.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr12",
    "rep": 3,
    "cost_usd": 0.0164,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Password hashing changed from PBKDF2-SHA256 (spec requirement) to MD5 (cryptographically broken). Violates SPEC.md section 3 and introduces critical security vulnerability. MD5 is unsuitable for password hashing and vulnerable to brute-force attacks.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0531,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0655,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr13",
    "rep": 3,
    "cost_usd": 0.0718,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.1415,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "medium",
        "issue": "count_my_orders doesn't check is_admin flag; violates spec that admins can reference all orders",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.1353,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr14",
    "rep": 3,
    "cost_usd": 0.1699,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "medium",
        "issue": "Per REVIEW.md requirement: 'Functions that return user-ID-derived data should verify the owner.' Function lacks authorization check, could allow accessing another user's order count.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.1138,
    "turns": 25,
    "tool_calls": 24,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.1088,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "haiku",
    "cond": "D",
    "pr": "pr15",
    "rep": 3,
    "cost_usd": 0.119,
    "turns": 14,
    "tool_calls": 13,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0076,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0028,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0113,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Pagination offset is computed as page * per_page, but page defaults to 1 and is 1-based. Page 1 therefore skips the first per_page orders, and those orders can never be reached. The offset should be (page - 1) * per_page.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The test assertion `0 < len(page1) <= 10` is too weak to catch the off-by-one offset bug. It passes even when page 1 returns the wrong rows. It should check the actual order IDs returned, for example that page 1 contains the first 10 orders.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0046,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Offset is computed as page * per_page, but pages default to 1-based (page=1 default). Page 1 therefore skips the first per_page orders, so the first page of results is never returned. It should be (page - 1) * per_page. The test only asserts 0 < len(page1) <= 10, so it does not catch this.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The pagination assertion is too weak (0 < len <= 10). It does not verify that page 1 starts at the first order, so the off-by-one offset bug passes the test. Assert the actual order IDs returned.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0133,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "medium",
        "issue": "`user[\"nickname\"].strip()` raises AttributeError when nickname is NULL/None, which is the usual value for users who never set one. `order_summary` would then crash for those users instead of falling back to `name`. Guard it with `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The assertion `\"Al\" in summary.order_summary(...)` is too weak. If the user's name is \"Alice\", it passes even when the nickname is ignored. The test also never covers a NULL or blank nickname, which is the fallback path that can crash. Assert on the exact expected display name and add cases for blank and None nicknames.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0125,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "medium",
        "issue": "`user[\"nickname\"].strip()` raises AttributeError when the nickname is NULL/None. Any user without a nickname (an optional column is typically NULL) makes order_summary crash instead of falling back to `user[\"name\"]`. The `or user[\"name\"]` fallback only covers empty or whitespace strings, not None. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The test is named test_summary_uses_nickname, but it asserts only `\"Al\" in ...`. \"Al\" is also a substring of the name \"Alice\", so the test passes even if the nickname is ignored. It also does not cover the empty-nickname or NULL-nickname fallbacks. Assert on the exact nickname, and set the nickname explicitly in the test.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0093,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "The atomic conditional UPDATE was replaced with a read-then-write (SELECT, then UPDATE with a computed absolute qty). Under concurrent reservations, two transactions can both read the same qty, both pass the check, and the second write overwrites the first. This causes lost updates and overselling, so stock goes negative or is under-decremented. It is also a check-then-act race with no lock or transaction guard. The original single-statement `UPDATE ... WHERE qty >= ?` was race-free.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "low",
        "issue": "`row[\"qty\"]` requires conn.row_factory to be sqlite3.Row (or similar). The original code did not depend on that. With default tuple rows this raises TypeError. Use row[0] or ensure the factory is set everywhere.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0043,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "The atomic conditional UPDATE was replaced by a read-then-write (SELECT, then UPDATE with a computed absolute qty). Under concurrent reservations, two transactions can both read the same qty and both pass the check, so the second write overwrites the first (lost update). This causes overselling or stock being under-decremented. The original `UPDATE ... WHERE qty >= ?` was race-free and should be restored.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "low",
        "issue": "`row[\"qty\"]` requires conn.row_factory to be sqlite3.Row (or similar). With the default tuple rows this raises TypeError. The original code did not depend on this connection setting.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0133,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "high",
        "issue": "update_order_note fetches the order with db.get_order but never checks that it belongs to `user`. Any authenticated user can overwrite the note on any other user's order just by knowing or guessing the order id (an IDOR). The `user` argument is never used. The test suite already uses `orders.get_order_for_user`, which suggests an ownership-checked path exists. Compare the owner (e.g. order['user_id'] != user['id']) or reuse that helper, and raise NotFound or a permission error on mismatch.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0066,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to `user`. The `user` argument is unused, so any user can overwrite the note on any other user's order by passing its id. This is an authorization bypass (IDOR). The function should compare `order[\"user_id\"]` with `user`'s id, as the order lookup and cancel paths appear to do, and raise NotFound or a permission error on mismatch.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0108,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A user-supplied keyword like `' OR 1=1 --` or a UNION payload breaks out of the LIKE clause, bypasses the `user_id` filter and can read other users' orders or other tables. Use a parameterized query, e.g. `conn.execute(\"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\", (user_id, f'%{keyword}%'))`. Also, a keyword containing a single quote causes a syntax error even without malicious intent.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0041,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A user-supplied keyword like `' OR 1=1 --` or a UNION payload escapes the user_id filter and exposes other users' orders or other tables. Use a parameterized query: `note LIKE ?` with `f'%{keyword}%'` as the parameter. Also, LIKE wildcards (% and _) in the keyword should be escaped. A keyword containing a single quote also causes a syntax error.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0109,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key (sk_live_...) is hardcoded and committed to source control. Anyone with repo access can use it, and it stays in git history. Read it from an environment variable (like DB_PATH does) and rotate the exposed key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "medium",
        "issue": "The email text says '合計 {total} セント' (cents), but the shop config uses FREE_SHIPPING_THRESHOLD=5000 and SHIPPING_FEE=500, and the Japanese text suggests a yen-like currency. The unit is hard-coded and may be wrong, so customers could see an incorrect amount (e.g. 900 shown as 900 cents instead of 9.00). Format the amount using the actual currency/unit.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0049,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key (sk_live_...) is hardcoded and committed to source control. Anyone with repo access can use it, and it stays in git history. Read it from an environment variable (like DB_PATH does) and rotate the exposed key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "medium",
        "issue": "The email text says '合計 {total} セント' (total in cents), but config uses FREE_SHIPPING_THRESHOLD=5000 and SHIPPING_FEE=500 in an unspecified currency unit, and the shop is Japanese-language. Hardcoding 'cents' without formatting may show customers a wrong or misleading amount (e.g., 900 shown as 900 cents rather than a formatted price).",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0136,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "low",
        "issue": "coupon_percent is not validated. A value above 100 makes the subtotal negative, and a negative value turns the coupon into a surcharge. A negative subtotal also goes into shipping_fee and can produce a negative or incorrect order total. Reject or clamp values outside 0..100.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0082,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "medium",
        "issue": "coupon_percent is not validated. A value above 100 makes the subtotal negative, and a negative value raises the price. A negative subtotal also feeds into shipping_fee, which may then charge or waive shipping incorrectly. Any caller passing a bad or user-supplied coupon value gets a wrong or negative total, so the value should be rejected or clamped to 0-100.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0118,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The guard changed from 'only pending orders can be cancelled' to 'reject only if already cancelled'. Orders in any other state (shipped, delivered, completed, refunded, etc.) can now be cancelled. Their stock is then released back into inventory even though the goods have already left, which inflates stock counts. The status check and the status update are also not atomic. Restore the `!= \"pending\"` check, and keep the 'already cancelled' message only if a distinct error for that case is wanted.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0049,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The guard was loosened from 'only pending' to 'not already cancelled', so orders in other states (shipped, delivered, completed, refunded) can now be cancelled. cancel_order then sets status to 'cancelled' and calls inventory.release_stock, which returns stock for goods that were already shipped or delivered. This inflates inventory and permits cancelling fulfilled orders. The original pending-only check should be kept, and the double-cancel case is already rejected by it, though with a different message.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 39,
        "severity": "low",
        "issue": "The new test only covers double cancel. No test checks that non-pending states such as shipped are still rejected, so the regression above would go undetected. The test also uses pytest.raises, and the visible diff does not show whether pytest is imported in this file.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0126,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "medium",
        "issue": "reserve_stock now returns the remaining quantity (int) or None instead of a bool. A successful reservation that takes the last units returns 0, which is falsy. Any existing caller that uses `if reserve_stock(...)` or `if not reserve_stock(...)` will treat that success as a failure. The stock has already been decremented at that point, so the order is rejected while the units stay reserved. Callers were not part of this diff, so they need checking and updating to use `is not None`. The test also doesn't cover the case where remaining is 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0074,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "high",
        "issue": "reserve_stock used to return a bool and now returns the remaining quantity or None. When a reservation takes the last units, it returns 0, which is falsy. Any caller that checks `if reserve_stock(...)` or `if not reserve_stock(...)` will treat that successful reservation as a failure. It may then reject the order or roll back, leaving the stock decremented. Callers must compare with `is None` or the function must keep returning a bool. The diff does not update any callers, and the new test does not cover the remaining == 0 case.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.01,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "round_half_up now uses Python's built-in round(), which does banker's rounding (round half to even), so it no longer rounds half up. 12.5 becomes 12 instead of 13, and 0.5 becomes 0, while 1.5 and 2.5 become 2 and 2. The function name and the original behavior promise half-up, so discount amounts at exact .5 values change. Prices and discounts are affected for every order whose discount lands on a half. Results are also inconsistent: some halves round up and others round down, depending on parity. Keep int(x + 0.5) for non-negative values, or use decimal.ROUND_HALF_UP or math.floor(x + 0.5).",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "The test was changed to expect 12 for 12.5 so that it matches the new banker's rounding. This reverses the documented half-up requirement, and the test is named test_round_half_up. The assertion now hides the regression instead of catching it. It should still expect 13.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0054,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "round_half_up now uses Python's built-in round(), which does banker's rounding (round half to even), so it no longer rounds half up. For example, 12.5 gives 12 instead of 13, and 0.5 gives 0 instead of 1. The function name and its original behavior promise half-up, so discounts are now computed differently, and customers get a smaller discount on exact .5 values with an odd integer part. Other callers of round_half_up are affected too. Also, floating-point results like 125*0.10 make the result depend on representation. Restore int(x + 0.5), or better, use math.floor(x + 0.5) or Decimal with ROUND_HALF_UP.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "The test was changed to expect 12 for a 12.5 discount so it would pass with the new banker's rounding. This hides the regression and contradicts the test name test_round_half_up. The expectation should stay 13. The comment '12.5 -> 12' also contradicts half-up semantics.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0081,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "Password hashing was downgraded from PBKDF2-HMAC-SHA256 (200k iterations) to a single fast, unkeyed MD5 of salt+password. MD5 is cryptographically broken and extremely fast to brute-force on GPUs, so a leaked password table can be cracked quickly. Use a slow KDF such as PBKDF2, scrypt, bcrypt, or argon2 instead.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "The new hash output differs from the old PBKDF2 output, so verify_password will fail for every existing stored password hash. All existing users will be unable to log in, and no migration or fallback path is included.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0034,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "Password hashing was downgraded from PBKDF2-HMAC-SHA256 (200,000 iterations) to a single salted MD5 round. MD5 is fast and unsuitable for password storage, so a leaked database can be brute-forced at billions of guesses per second. Moreover, all existing stored hashes were created with PBKDF2, so verify_password will now compute MD5 and fail to match for every existing user, locking them out (no migration or fallback to the old scheme).",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0062,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0003,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0083,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0021,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0096,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "A",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0033,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0101,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "The PR says shipping is free when the subtotal is 10000 or more, but the code uses `subtotal > 10000`. A subtotal of exactly 10000 is charged 1200 instead of 0. It should be `>=`. The added tests only check 9999 and 20000, so they miss this boundary.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0034,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "The description says orders with a subtotal of 10000 cents or more get free shipping, but the code uses `subtotal > 10000`. A subtotal of exactly 10000 is charged 1200 instead of 0. The tests only cover 9999 and 20000, so they miss this boundary. Use `>=`, or confirm the intended threshold and fix the description.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0122,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Off-by-one in paging: page is documented as 1-based, but the offset is computed as page * per_page. Page 1 therefore skips the first per_page orders, so the first page of results is never returned. The offset should be (page - 1) * per_page. The test only asserts 0 < len(page1) <= 10, so it does not catch this.",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 47,
        "severity": "low",
        "issue": "page and per_page are not validated. page=0 gives offset 0 (or a negative offset if the formula is fixed to (page-1)*per_page), and a negative per_page makes SQLite treat LIMIT as unlimited. Either can return the whole table or behave unexpectedly.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0053,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Off-by-one in pagination: page is 1-based per the PR description, but the offset is computed as `page * per_page`. Page 1 therefore skips the first `per_page` orders and starts at row per_page, so the first page of results is never returned. It should be `(page - 1) * per_page`. Page 0 or negative values are also not validated, and would produce a negative offset or a wrong offset.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The test only asserts `0 < len(page1) <= 10`, which passes even with the off-by-one offset bug. It does not verify that page 1 contains the first orders (e.g. the lowest ids) or that pages don't overlap or skip rows, so it fails to catch the pagination bug.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0151,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "medium",
        "issue": "`user[\"nickname\"].strip()` raises AttributeError when nickname is NULL/None. The PR description says the nickname is shown only \"if there is one\", which implies the column is nullable. Any user without a nickname would then crash `order_summary` instead of falling back to `name`. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The test does not verify the nickname behavior. It never sets a nickname, and `\"Al\"` is also a substring of a name like \"Alice\", so it passes even if the nickname logic is broken or absent. It also does not cover the empty-nickname fallback or the NULL-nickname case.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0088,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "medium",
        "issue": "`user[\"nickname\"].strip()` raises AttributeError when nickname is NULL/None. The PR says the nickname is shown \"if there is one\", which suggests the column is nullable. Users without a nickname would crash `order_summary` instead of falling back to the name. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 40,
        "severity": "low",
        "issue": "The test asserts `\"Al\"` is in the summary. That substring probably also appears in the real name (e.g. \"Alice\"), so the test passes even without the nickname feature. It does not cover the empty-nickname or NULL-nickname fallback either. Set a distinct nickname and assert on it exactly, and add fallback cases.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.01,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "The atomic conditional UPDATE (qty >= ?) was replaced by a read-then-write (SELECT, then UPDATE with a computed absolute value). Two concurrent reservations can both read the same qty and both succeed, and the second write overwrites the first, so stock is oversold or decrements are lost. This contradicts the claim that behavior is unchanged. Restore the single atomic UPDATE ... WHERE qty >= ? and check rowcount, or wrap the operation in a transaction that locks the row (e.g. BEGIN IMMEDIATE).",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "medium",
        "issue": "row[\"qty\"] requires conn.row_factory to be sqlite3.Row (or similar). With the default tuple rows this raises TypeError (tuple indices must be integers). The original code did not depend on the row factory, so this may break existing callers.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0053,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "The atomic conditional UPDATE (qty >= ?) was replaced with a read-then-write (SELECT, then UPDATE with an absolute value computed in Python). Under concurrent requests, two callers can both read the same qty and both pass the check, then the second UPDATE overwrites the first. This causes lost updates and overselling, since stock can go negative or be reserved twice. This contradicts the claim that behavior is unchanged. Restore the single atomic UPDATE ... WHERE qty >= ? (and check rowcount), or use a transaction with a write lock (BEGIN IMMEDIATE).",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "medium",
        "issue": "row[\"qty\"] requires conn.row_factory to be sqlite3.Row (or similar mapping). The original code did not depend on this. With the default tuple row factory, this raises TypeError. It is a hidden dependency that can break callers or tests using a plain connection.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0147,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to `user`. The `user` argument is unused and the order is fetched with `db.get_order` by id alone. Any user can overwrite the note on another user's order by guessing or iterating order IDs. The test reads orders back through `get_order_for_user`, which suggests an ownership-checked lookup exists. Use it here, or compare `order['user_id']` to `user['id']`, and raise NotFound or a permission error on mismatch.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0083,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note takes `user` but never checks that the order belongs to them. It only checks that the order exists. Any authenticated user can overwrite the note on another user's order just by passing its order_id. cancel_order and get_order_for_user appear to enforce ownership, so this is an authorization bypass (IDOR). Fix: verify `order['user_id'] == user.id` (or reuse get_order_for_user) and raise NotFound or a permission error otherwise. The test only covers the owner, so this case is untested.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0107,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A user-supplied keyword like `' OR user_id LIKE '%` or `' UNION SELECT ...--` can break out of the LIKE clause, bypass the `user_id` filter and read other users' orders or other tables. Use a parameterized query (`note LIKE ?` with `f'%{keyword}%'` as the parameter). Also, a keyword containing a single quote causes a syntax error even in non-malicious use.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0045,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A user-supplied keyword like `' OR user_id LIKE '%` or `%' OR 1=1 --` breaks out of the LIKE clause and can return other users' orders (bypassing the user_id filter) or run arbitrary conditions. Use a parameterized query: `note LIKE ?` with `(user_id, f'%{keyword}%')`. Also a keyword containing a single quote causes a syntax error. Consider escaping `%`/`_` wildcards if literal matching is intended.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0124,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live API secret (sk_live_...) is hardcoded and committed to source control. Anyone with repo access can use it, and it stays in git history. Load it from an environment variable or secret store like DB_PATH does, and rotate the exposed key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "medium",
        "issue": "The email text says '合計 {total} セント' (total in cents), but the shop config uses thresholds like FREE_SHIPPING_THRESHOLD=5000 and SHIPPING_FEE=500 in an unspecified currency, and the message is in Japanese. If amounts are in yen, the unit is wrong and customers see an incorrect amount, e.g. 900 yen shown as 900 cents.",
        "label": "fp"
      },
      {
        "file": "shop/mailer.py",
        "line": 16,
        "severity": "low",
        "issue": "Network errors or non-2xx responses (urlopen raises URLError/HTTPError, timeout 5s) are not handled. If the mail API is down, send_order_confirmation raises and can break order completion for the caller, since the mail is sent synchronously.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0063,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live API secret (sk_live_...) is hardcoded and committed to source control. Anyone with repo access can use it, and it stays in git history. Load it from an environment variable or secret store (like DB_PATH does with os.environ), and rotate the exposed key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "medium",
        "issue": "The email body says '合計 {total} セント' (total in cents), but the rest of the shop config (FREE_SHIPPING_THRESHOLD=5000, SHIPPING_FEE=500) suggests a currency whose unit is not clearly cents, and the amount is not formatted. The currency unit is hardcoded in the customer-facing text and may show the wrong amount or unit to customers.",
        "label": "fp"
      },
      {
        "file": "shop/mailer.py",
        "line": 16,
        "severity": "medium",
        "issue": "urlopen raises on network errors and HTTP errors (URLError/HTTPError/timeout) and nothing handles them. If send_order_confirmation is called in the order-completion flow, a mail outage would make order completion fail or raise after the order is committed. Callers need error handling, or the failure should be caught and logged.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0137,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.01,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "medium",
        "issue": "coupon_percent is not validated. A value above 100 makes the subtotal negative, which can produce a negative or refunded total, and a negative value raises the price. Callers can pass untrusted input, so reject values outside 0..100 (raise ValueError).",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0101,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The check was changed from `status != \"pending\"` to `status == \"cancelled\"`, which breaks the stated rule that only pending orders can be cancelled (the PR description says this rule is unchanged). Orders that are shipped, delivered, paid or completed can now be cancelled. Their status is overwritten to \"cancelled\" and stock is released back into inventory even though the goods have already left, so inventory is overstated. The fix is to keep the `!= \"pending\"` check and only change the message, or to return \"already cancelled\" for the cancelled case and still reject every other non-pending status.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0041,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The check changed from `status != \"pending\"` to `status == \"cancelled\"`, which breaks the spec that only pending orders can be cancelled (the PR description says this was not changed). Orders in other states, such as shipped, delivered or paid, can now be cancelled. Their stock is then released back to inventory even though the goods have already shipped, which corrupts inventory counts and order state. The check should stay `!= \"pending\"`, with the \"already cancelled\" message added as a separate branch for cancelled orders.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0135,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "medium",
        "issue": "The return type changed from bool to int | None. A reservation that uses up the last unit now returns 0, which is falsy. Any existing caller that checks truthiness, such as `if reserve_stock(...)` or `if not reserve_stock(...)`, will treat this successful reservation as a failure. It may then refuse the order or roll back, even though the stock was already decremented, leaving the stock and the order inconsistent. Callers must be updated to use `is None` / `is not None`, or the function should keep returning a bool. The new test also never covers the case where the remaining stock is 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0082,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "high",
        "issue": "The return type changed from bool to int | None. When a reservation takes the last units, the remaining stock is 0. A successful reservation then returns 0, which is falsy. Existing callers that use `if reserve_stock(...)` or `if not reserve_stock(...)` will treat that success as a failure, for example by rejecting the order or rolling back. Callers must now check `is None`. The change also silently breaks the old bool contract for every other caller, and the diff updates none of them. Reserving exactly the remaining stock is the case that triggers this. The new test does not cover it, since it only checks remaining values of 1 and None.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0113,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "`round_half_up` now calls Python's built-in `round`, which uses banker's rounding (half to even) instead of half up. The function name and its documented behavior no longer match: 12.5 becomes 12, 0.5 becomes 0, 2.5 becomes 2, while 13.5 becomes 14. Discount amounts at exact .5 boundaries change, so the discount amount is rounded down in some cases and up in others, depending on parity. This silently changes the amounts customers are charged. The PR changes the test expectation to match the new behavior instead of preserving the existing requirement, and the PR description does not justify a pricing change. Either keep the original half-up implementation (or use `decimal` with ROUND_HALF_UP) or rename the function and get explicit sign-off on the pricing change.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "medium",
        "issue": "The test was edited to expect 12 for 12.5 while the test name and function are still `round_half_up`, so the test now asserts behavior contradicting its own name. It hides the regression rather than guarding the half-up requirement. It also covers only one case, so it would not catch the parity-dependent behavior (e.g. 13.5 gives 14, 12.5 gives 12).",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0065,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "`round_half_up` now calls Python's built-in `round`, which uses banker's rounding (round-half-to-even), so the function no longer does what its name says. 12.5 now gives 12 instead of 13, and 0.5 gives 0 instead of 1. Discount amounts at exact .5 boundaries change, with half-values rounding down on even integers. This is a silent change to money calculations. The test expectation was edited to match the new behavior instead of keeping the half-up requirement. Also, `total * rate` is a float, so results can be inconsistent near halves. Keep the half-up implementation, e.g. `int(x + 0.5)` or `decimal.ROUND_HALF_UP`, or rename the function and get explicit sign-off on the rounding policy change.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "medium",
        "issue": "The test was changed to expect 12 for 12.5, which contradicts the function name `round_half_up` and the original requirement (12.5 -> 13). The change encodes the regression as expected behavior. The assertion comment `# 12.5 -> 12` also documents banker's rounding under a half-up name. No test covers other half cases (e.g. 13.5 -> 14 under both policies, 0.5, 2.5) that would expose the even/odd inconsistency.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0102,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "Password hashing was replaced with a single-round MD5(salt+password). MD5 is a fast hash, so a leaked database can be brute-forced at billions of guesses per second on a GPU. The 200,000-iteration PBKDF2 existed specifically to make that expensive, and a single salted MD5 gives almost no protection against offline cracking. A faster login is not a valid reason to drop the work factor. If login is slow, fix it elsewhere (for example by lowering the iteration count moderately, or using a purpose-built KDF such as argon2id, scrypt or bcrypt) instead of using a general-purpose fast hash.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "The hash function changed without any migration or versioning. Every existing stored PBKDF2 hash will now fail verify_password, because it recomputes with MD5 and compares it against the PBKDF2 output. All existing users will be locked out of their accounts until they reset their passwords. A scheme/version marker is needed so old hashes can be verified and then re-hashed on a successful login.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0057,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 14,
        "severity": "high",
        "issue": "Password hashing was changed from PBKDF2-HMAC-SHA256 (200,000 iterations) to a single round of MD5(salt+password). MD5 is a fast, general-purpose hash, so a leaked database can be brute-forced at billions of guesses per second on a GPU. This removes the intentional work factor that makes password hashing resistant to offline cracking. The slowness complained about is the security feature. Keep a slow KDF (PBKDF2 with a high iteration count, scrypt, bcrypt or argon2) and fix the login latency another way, such as running the hash off the request path or tuning the parameters.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 14,
        "severity": "high",
        "issue": "Existing stored hashes were produced with PBKDF2 and are not migrated. verify_password now computes MD5 and compares it with the old PBKDF2 digest, so every existing user will fail to log in after deploy. There is no versioned hash format or fallback that verifies the old hash and rehashes on successful login.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0066,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0018,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0086,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0022,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0068,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "B",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0022,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0243,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "`express_shipping_fee` 内の比較が `subtotal > 10000` になっています。PR の説明では「10000セント以上なら無料」なので、`>=` にする必要があります。小計がちょうど 10000 のとき、無料のはずが 1200 セントを請求します。既存の `shipping_fee` は仕様どおり「以上」で無料にしているため、挙動も食い違います。追加されたテストは 9999 と 20000 しか見ておらず、境界値の 10000 では失敗を検出できません。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0054,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "medium",
        "issue": "Off-by-one at the boundary. The PR description says a subtotal of 10000 or more ships free, but `subtotal > 10000` charges 1200 when the subtotal is exactly 10000. It should be `>= 10000`. The existing shipping rule in SPEC.md is also 'or more', and the new tests (9999 and 20000) don't cover the 10000 boundary.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0194,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "页码是从1开始的,但偏移量计算为 page * per_page。第1页会跳过前 per_page 条记录,最早的订单永远无法返回。偏移量应为 (page - 1) * per_page。当总数不超过 per_page 时,第1页会返回空结果。",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 45,
        "severity": "low",
        "issue": "断言 `0 < len(page1) <= 10` 过于宽松,无法发现偏移量错误(例如第1页缺少最早的订单)。应该检查具体的订单 id,例如第1页是否包含最小的 id。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0081,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "`page` は1始まりなのに、オフセットを `page * per_page` と計算しています。`page=1` で先頭の `per_page` 件を飛ばし、1ページ目が実際の2ページ目になります。最初のページは取得できません。正しくは `(page - 1) * per_page` です。`page=0` や負数の検証もないため、負のオフセットになる可能性があります。追加されたテストは `0 < len(page1) <= 10` としか確認しておらず、このバグを検出できません。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0161,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "`nickname` is a nullable column (`nickname TEXT` in db.py, and the test fixture inserts bob with NULL). `user[\"nickname\"].strip()` raises AttributeError (`NoneType` has no `strip`) for any user without a nickname. `order_summary` therefore crashes for those users, even though the PR says to fall back to the name. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0098,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "The `nickname` column is nullable. The schema in db.py defines it as `TEXT` with no NOT NULL, and the test fixture inserts bob with NULL. For any user with a NULL nickname, `user[\"nickname\"].strip()` raises AttributeError, so `order_summary` crashes. The PR says to fall back to the name when there is no nickname. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "low",
        "issue": "The new test only covers a user with a nickname. It has no case for a NULL or empty nickname, which is where the crash is. The assertion `\"Al\" in ...` is also weak: the name 'alice' contains 'al' but not 'Al', so the check is acceptable, but it does not verify that the nickname replaced the name.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0169,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "`reserve_stock` now does SELECT, checks the quantity in Python, then writes an absolute value (`qty = row[\"qty\"] - qty`). This is a read-modify-write race, and the original single `UPDATE ... WHERE qty >= ?` was atomic. Two concurrent orders can both read the same quantity and both pass the check. One then overwrites the other's decrement (lost update), so stock is over-reserved. With stock 5, two orders for 3 can both succeed and leave qty at 2. That violates SPEC §2 (no oversell under concurrent orders). The PR claims no behavior change, but this one is a regression. Fix by restoring the conditional atomic UPDATE and checking rowcount, with the existence check done as a separate step if needed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0091,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "`reserve_stock` now reads the quantity with SELECT and then writes an absolute value computed in Python (`row[\"qty\"] - qty`). Previously a single atomic `UPDATE ... WHERE qty >= ?` did the check and the decrement. Two concurrent orders can both read the same qty and both pass the check. The second UPDATE then overwrites the first, so stock is lost and the item is oversold. SPEC section 2 says concurrent orders must never make stock negative. This contradicts the PR description's claim that behavior is unchanged. Restore the atomic conditional UPDATE and check `rowcount`.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0293,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "`update_order_note` 関数は `db.get_order` を直接呼んでおり、所有者チェックをしていません。そのため、ログインしているユーザーなら誰でも他人の注文のメモを書き換えられます。SPEC §3 は、ユーザーが操作できるのは自分の注文だけと定めています。同じファイルの `cancel_order` は `get_order_for_user` を使っています。この関数も `get_order_for_user(conn, user, order_id)` を使えば、他人の注文には Forbidden が返ります。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0104,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "update_order_note は注文の所有者を確認していません。`user` 引数を使っておらず、`order[\"user_id\"]` との比較もしていません。ログインしているユーザーなら、他人の注文のメモを order_id を指定して上書きできます。SPEC §3 は、ユーザーが操作できるのは自分の注文だけと定めており、これに違反します。`get_order_for_user` を使うか、所有者チェックを行い、所有者でない場合は Forbidden を送出する必要があります。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0144,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "search_orders builds SQL with an f-string and puts `keyword` into it unescaped. This is SQL injection, and it violates the spec requirement that all SQL be parameterized (SPEC §4). A keyword such as `' OR user_id<>1 --` or a UNION payload can break out of the `user_id` filter and return other users' orders, which also violates the authorization requirement. A keyword containing a single quote causes a syntax error. Use `note LIKE ?` with a parameter such as `f'%{keyword}%'`, and escape the `%` and `_` wildcards if the match should be literal.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0053,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 56,
        "severity": "high",
        "issue": "search_orders builds SQL by interpolating `keyword` directly into an f-string, which allows SQL injection. A keyword such as `' OR '1'='1` or a UNION payload can return other users' orders and bypass the `user_id` filter, or read other tables. This violates the SPEC requirement that all SQL be parameterized. Use `note LIKE ?` with a parameter of `f'%{keyword}%'`. Also escape `%` and `_` in the keyword if literal matching is intended.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0141,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key is hard-coded and committed to the repository. This violates the SPEC requirement that secrets be read from environment variables. The key is now exposed in version control and should be rotated. Read it with os.environ.get(\"MAIL_API_KEY\") instead.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The email body says \"合計 {total} セント\" (total in cents), but it does not format the amount. The `send_order_confirmation` function is also never called from the order-completion flow, even though the PR description says it sends mail when an order completes.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0058,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key is hardcoded and committed to the repository. SPEC §3 requires secrets to come from environment variables, as DB_PATH does. Anyone with repo access can use the key. Read it with os.environ.get(\"MAIL_API_KEY\") and rotate the leaked key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The email body says \"合計 {total} セント\" and passes the raw integer cents, so it is consistent with the spec. However, `send_order_confirmation` is never called from the order-completion flow. The PR description says it sends mail when an order completes, but nothing calls it.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0214,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "SPEC.md では、クーポンとまとめ買い割引は併用できません。両方に該当する場合は、割引額が大きい方だけを適用します。しかしこのコードは、まとめ買い割引を適用した後の小計にクーポンをさらに掛けています。数量10個以上の行があるとき、2つの割引が重なって適用されます。そのため、顧客に請求する額が仕様より少なくなります。行ごとのまとめ買い割引の合計と、クーポンを全体の総額(割引前)に適用した割引額を比べ、大きい方だけを適用する必要があります。",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 24,
        "severity": "medium",
        "issue": "テストは、まとめ買い割引の対象外である数量2のケースしか確認していません。そのため、併用禁止のルールが破られていても検出できません。まとめ買い割引とクーポンの両方に該当するケースのテストがありません。送料の境界値(5000セント)のテストもありません。",
        "label": "valid"
      },
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "low",
        "issue": "coupon_percent を検証していません。負の値や100を超える値を渡すと、合計が増えたり、負の金額になったりします。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0099,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "SPEC.md says a coupon cannot be combined with the bulk discount. When both apply, only the larger discount may be used. This code subtracts the bulk discount from each line (lines 47-48) and then applies the coupon to the already-discounted subtotal, so the two stack. For example, 1000 x 10 with a 10% coupon gives 8100, but the spec gives 8000 (bulk 10% only, since the coupon is not larger). The result is under-charged. The correct approach is to compare the total bulk discount with the coupon discount on the gross subtotal and apply only the larger one. The same comparison is needed for per-line vs order-level discounts. Shipping is also computed on the wrongly reduced subtotal.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "medium",
        "issue": "The only coupon test uses a quantity of 2, so no bulk discount applies. It does not cover the coupon and bulk discount case from the spec (only the larger discount applies), which is why the stacking bug is not caught. It also has no test for the free-shipping threshold.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0135,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The status check changed from `!= \"pending\"` to `== \"cancelled\"`. Orders in other states, such as shipped, delivered or paid, can now be cancelled. This violates the spec (section 4), which allows cancelling only pending orders, and contradicts the PR description. Cancelling such an order also releases stock that was already shipped or consumed, so inventory is inflated. The fix is to keep the `!= \"pending\"` check and choose the error message by status, for example 'already cancelled' when status is 'cancelled' and 'only pending orders can be cancelled' otherwise.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0052,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The status check was changed from `!= \"pending\"` to `== \"cancelled\"`. Orders in any other status, such as shipped or completed, can now be cancelled. This violates SPEC §4, which says only pending orders can be cancelled. Stock is also released for those orders, which corrupts inventory. The PR description says the pending-only rule is unchanged, but the code no longer enforces it. Keep the pending check and give non-pending orders a clearer message, for example 'already cancelled' only when the status is cancelled.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0232,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "`create_order` 内の `if not inventory.reserve_stock(...)` は、戻り値が bool から「残り在庫数 or None」に変わったことに対応していない。在庫をちょうど使い切る注文では残りが 0 になる。0 は falsy なので、在庫の引き当ては成功してコミット前に在庫が減っているのに OutOfStock が送出される。最後の1個、または残りちょうどの数量が注文できなくなる。さらに注文は作られず、在庫だけが減ったままになる可能性がある。SPEC 第2節違反。`is None` で判定するよう修正が必要。",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "low",
        "issue": "UPDATE と、残り在庫を読む SELECT が別々の文になっている。同時注文があると、返る「残り在庫数」が他のトランザクションの更新後の値になることがある。テストも在庫を使い切るケースを含んでいない。",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0128,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "high",
        "issue": "`reserve_stock` now returns the remaining stock count, which is 0 when the last units are reserved. The caller in `shop/orders.py:18` still uses `if not inventory.reserve_stock(...)`. A successful reservation that sells out the stock returns 0, which is falsy, so `create_order` raises OutOfStock. The caller never checks `is None`. The stock was already decremented, and the transaction is not rolled back or committed, so the order fails and the stock is left in an inconsistent state. This breaks SPEC §2, which says an order should succeed when stock is sufficient. The caller should use `is None`, and the tests should cover a reservation that leaves 0.",
        "label": "hit"
      },
      {
        "file": "tests/test_inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "The test never reserves down to exactly 0 remaining. That is the case where the new return value is falsy and breaks the caller `create_order`, so the regression is not caught.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0133,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round-half-to-even), but SPEC requires half-up rounding. For example, 12.5 gives 12 instead of 13, and 2.5 gives 2 instead of 3. The function is still named round_half_up but no longer does half-up. Every discount that lands on a .5 value is now computed wrongly, and the customer gets less discount. Floating-point error in total*rate can also affect the result. The original int(x+0.5) was correct for non-negative values.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "medium",
        "issue": "The test expectation was changed from 13 to 12 to match the new buggy behavior. This contradicts the SPEC (0.5 rounds up), and the comment '12.5 -> 12' is wrong. The test should keep expecting 13, so the regression is now hidden.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0057,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round half to even), not half up. SPEC.md requires 0.5 to round up. discount_amount(125, 0.10) now returns 12 instead of 13, and any exact .5 case such as 2.5 gives 2 instead of 3. This under-discounts or over-discounts by 1 cent depending on the value. The function name `round_half_up` is now also wrong.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "The test expectation was changed from 13 to 12 to match the new behavior. This contradicts the spec, which says half-up rounding (12.5 -> 13), so the test now locks in the bug instead of catching it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0117,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "パスワードのハッシュをPBKDF2-SHA256(200,000回)からsalt+passwordの単純なMD5に置き換えている。MD5は高速すぎるため、DBが漏洩するとGPUによるブルートフォース/辞書攻撃で容易にパスワードが解読される。SPEC §3の「salt付きPBKDF2-SHA256で保存する」に違反している。また、既存ユーザーの保存済みPBKDF2ハッシュとは一致しないため、移行処理がなければ全員がログインできなくなる。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0054,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "パスワードのハッシュが salt 付き PBKDF2-SHA256(200,000回)から MD5(salt+password) に置き換わっています。MD5 は高速なので、DB が漏えいするとオフラインのブルートフォース攻撃やレインボーテーブル攻撃に弱くなります。SPEC §3 の「salt 付き PBKDF2-SHA256 で保存する」に違反します。また、既存ユーザーの保存済み PBKDF2 ハッシュは新しい MD5 の結果と一致しません。移行や再ハッシュの仕組みもないため、既存ユーザー全員がログインできなくなります。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0108,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0035,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.012,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0034,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0158,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0077,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0147,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "medium",
        "issue": "境界値がPR説明と食い違っています。説明では小計が10000セント以上なら無料ですが、`subtotal > 10000` では小計がちょうど10000のとき1200セントを請求します。`>=` にする必要があります。テストは9999と20000しか確認していないため、この不具合は検出されません。",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 27,
        "severity": "low",
        "issue": "テストが境界値の10000を検証していません。9999と20000だけでは、`>` と `>=` の違いを検出できません。10000のケースを追加してください。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0093,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "The code uses `subtotal > 10000`, but the PR description says a subtotal of 10000 cents or more ships free. At exactly 10000 the function returns 1200 instead of 0, so the customer is overcharged. This is an off-by-one at the boundary, which REVIEW.md says to check word by word against the spec. The fix is `>=`.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 26,
        "severity": "low",
        "issue": "The test checks 9999 and 20000 but not the 10000 threshold. This is why the `>` vs `>=` bug passes. REVIEW.md requires tests to check boundary values directly, so add `express_shipping_fee(10000) == 0`.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.019,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "ページは1始まりなのに offset を page * per_page で計算しています。page=1 のとき offset は per_page になり、最初の per_page 件を飛ばします。offset は (page - 1) * per_page にしてください。page=0 や負の値を渡した場合の検証もありません。",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "medium",
        "issue": "テストは 0 < len(page1) <= 10 しか確認していません。page=1 が先頭の注文から始まるかを確認していないため、offset のずれを検出できません。また、最終ページや重複なしのページ分割も検証していません。ID や先頭要素を直接確認する必要があります。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0101,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "The offset is `page * per_page`, but pages are 1-based (SPEC §4 and the PR description). `page=1` therefore skips the first `per_page` orders and returns the second page. The last page is also unreachable, and `page=0` or a negative page gives a wrong or negative offset. The offset should be `(page - 1) * per_page`, with `page >= 1` validated.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 44,
        "severity": "medium",
        "issue": "The test only asserts `0 < len(page1) <= 10`. It does not check that page 1 contains the first orders (the lowest ids), so it passes with the off-by-one offset bug. It should assert the exact ids on page 1 and on page 3, which should hold 5 orders.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0204,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "nickname 列は NULL を許容します(db.py のスキーマ、およびテストの bob は nickname=NULL)。そのため user[\"nickname\"] が None のとき `.strip()` が AttributeError になります。ニックネーム未設定のユーザーの注文サマリはすべてクラッシュします。`(user[\"nickname\"] or \"\").strip() or user[\"name\"]` のように修正してください。",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "low",
        "issue": "テストが確認しているのはニックネームありの場合だけです。ニックネームが NULL の場合と空文字の場合のフォールバックは検証していません。NULL のケースを検証していれば、上のクラッシュを検出できたはずです。また、`\"Al\"` は部分文字列の一致なので弱い検証です。たとえば \"alice\" 内の \"al\" は大文字小文字が違うので一致しませんが、出力全体や名前が含まれないことまでは確認できません。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0104,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "The `nickname` column is nullable (`nickname TEXT`), and the test fixture inserts user bob with `nickname` NULL. For such a user, `user[\"nickname\"]` is None, so `.strip()` raises AttributeError and `order_summary` crashes. The PR says to use the user name when no nickname is set. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "low",
        "issue": "The new test only covers a user who has a nickname. It does not cover a NULL nickname (bob) or an empty or whitespace nickname, which are the fallback cases the PR describes. That is why the NULL crash is not caught. Also, `\"Al\" in ...` is a weak assertion, since the substring could match other parts of the output.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0161,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "reserve_stock が SELECT してから UPDATE する read-modify-write になっている。同時注文が同じ qty を読み取ると、後からの UPDATE が先の減算を上書きする(lost update)。在庫が二重に引き当てられ、実際より多く売れてしまう。また絶対値 `qty = ?` で書くため在庫がマイナスになる可能性もある。SPEC 第2節(同時注文で在庫がマイナスにならない)と REVIEW.md(条件付きUPDATEで行う、read-modify-write禁止)に違反する。元の `UPDATE ... WHERE qty >= ?` と rowcount の判定に戻すこと。PR の「挙動は変えていない」という説明は誤り。",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "row[\"qty\"] で列名アクセスしている。conn.row_factory が sqlite3.Row に設定されていない場合は TypeError になる。shop/db.py 経由の接続でその設定があるか確認が必要。",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0071,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "reserve_stock was changed from an atomic conditional UPDATE (qty >= ?) to SELECT followed by an UPDATE that writes an absolute value (read-modify-write). With concurrent orders, two transactions can both read the same qty and both pass the check. One then overwrites the other's decrement (lost update) or oversells, so stock can go negative or be wrong. This violates SPEC 2 (stock must never go negative under concurrent orders) and the REVIEW.md rule against read-modify-write. The PR description says behavior is unchanged, which is wrong. Restore the single `UPDATE ... WHERE sku=? AND qty>=?` and check rowcount.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "row[\"qty\"] only works if conn.row_factory is sqlite3.Row. If the connection from shop/db.py doesn't set it, this raises TypeError on tuple rows. The old code did not depend on this.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0217,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to `user`. It calls db.get_order directly, so any logged-in user can overwrite the note on another user's order by passing its order_id. This breaks SPEC §3 (users may access only their own orders) and the REVIEW.md rule that authorization is checked at function entry. Use get_order_for_user, or compare order['user_id'] with user['id'] (admins may be exempt).",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0055,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "update_order_note does no ownership check. `user` is never used, so any user can overwrite the note on any other user's order if they know or guess the order_id. SPEC §3 says users may only access their own orders, and REVIEW.md requires an owner check at the entry of functions in orders.py. Reuse get_order_for_user, or compare order['user_id'] with user['id'] and allow admins as appropriate.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0164,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string with an f-string. A keyword like `' OR '1'='1` or a UNION payload breaks out of the LIKE clause. This can return other users' orders or other tables' data, bypassing the user_id filter. SPEC §4 and REVIEW.md require parameterized SQL with `?` placeholders. Use `note LIKE ?` with `f'%{keyword}%'` as the parameter. Escape `%` and `_` in the keyword if literal matching is intended.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0059,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A keyword such as `' OR '1'='1` or a UNION payload bypasses the user_id filter and returns other users' orders or other data. SPEC §4 and REVIEW.md require parameterized SQL with `?` placeholders. Use `note LIKE ?` with `f'%{keyword}%'` as the parameter, and escape `%` and `_` in the keyword if the match is meant to be literal.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0165,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live API key (sk_live_...) is hardcoded and committed to the repository. SPEC §3 and REVIEW.md require secrets to come from environment variables. Anyone with repo access can use the key. Read it with os.environ.get(\"MAIL_API_KEY\") and rotate the leaked key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The email body says \"合計 {total} セント\", which shows the raw integer cents. The wording is the only unit shown, so it is probably fine. send_order_confirmation is also not called from any order-completion code, so the PR description's claim that it sends mail on order completion is not implemented.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0065,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key (sk_live_...) is hardcoded and committed. SPEC section 3 and REVIEW.md require secrets to come from environment variables via os.environ.get. Anyone with repo access can use the key. Rotate it and read it from an env var.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The mail text says '合計 {total} セント', but nothing in this PR checks that total is an integer number of cents. Also, send_order_confirmation is never called from the order flow, even though the PR says it sends mail on order completion. In addition, _post has no error handling: a mail API failure raises URLError or HTTPError, which would break order completion if the function is later wired in.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0239,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "SPEC.md では、クーポンとまとめ買い割引は併用できません。両方が該当する場合は、割引額が大きい方だけを適用します。この実装は、行ごとのまとめ買い割引を引いた後の小計にクーポンをさらに適用しています。そのため、数量が10個以上の行があってクーポンも使う注文では、割引が二重にかかり、合計が仕様より安くなります。例えば 1000x10 に10%クーポンを使うと、仕様では 9000 になるはずですが、この実装では 8100 になります。",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 24,
        "severity": "medium",
        "issue": "テストが確認しているのは、数量2でまとめ買い割引がかからないケースだけです。仕様の中心である「併用不可で割引額が大きい方を適用する」ケースと、送料無料の境界(5000以上)は確認していません。そのため、上の併用の不具合を検出できません。",
        "label": "valid"
      },
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "low",
        "issue": "割引額の計算に float を使っています(coupon_percent / 100 と total * rate)。REVIEW.md の「float演算の結果を金額にしない」に反します。half-up の端数処理も float に依存するため、x.5 の境界で誤差により丸めが狂う可能性があります。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0145,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "SPEC 第1節では、クーポンとまとめ買い割引は併用できず、両方に該当する場合は割引額が大きい方だけを適用すると定められている。この実装は、まとめ買い割引を適用した後の小計にクーポンをさらに適用しているため、両方が重なる注文では割引が二重にかかる。例えば 1000×10個に10%クーポンを使うと、仕様では 9000 になるが、この実装では 8100 になる。",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 24,
        "severity": "medium",
        "issue": "テストはまとめ買い割引が付かない数量(2個)だけを検証している。併用不可という仕様の中心部分と、割引額の大小比較(まとめ買い割引が大きい場合とクーポンが大きい場合)が検証されていない。送料の境界値である 5000 セントちょうどのケースも検証されていない。そのため、併用の不具合を検出できない。",
        "label": "valid"
      },
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "low",
        "issue": "coupon_percent / 100 は float で、total*rate の計算も float 演算になる。REVIEW.md は金額に float 演算の結果を使うことを禁じている。整数演算 (total*percent+50)//100 を使うべき。また coupon_percent が 0〜100 の範囲外でも検証されないため、負の値では価格が上がり、100 を超えると小計が負になる。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0163,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The status check changed from `!= \"pending\"` to `== \"cancelled\"`. Orders in other non-pending states, such as shipped, paid or completed, can now be cancelled. SPEC §4 says only pending orders can be cancelled, and the PR description says this was not changed. Cancelling such an order also releases stock that should not be returned, which corrupts inventory counts. The original check should be restored. The 'already cancelled' message can be kept by checking for it first and then keeping the `!= \"pending\"` check.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The new test covers only a double cancel. No test checks that a non-pending order, such as shipped, is rejected, so the regression in the cancel check goes undetected. A test for that boundary should be added.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0066,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The status check now only rejects \"cancelled\". Orders in other non-pending states, such as shipped or paid, can now be cancelled, and their stock is released. This violates SPEC §4, which says only \"pending\" orders can be cancelled. The PR description says this rule is unchanged, but the code changes it. Stock is also restored for orders that were already fulfilled. The check should still be `!= \"pending\"`. The \"already cancelled\" message can be raised separately before it.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 40,
        "severity": "low",
        "issue": "The new test only covers double cancel. No test checks that cancelling a non-pending order, such as a shipped one, is rejected. That is the SPEC boundary, and this regression would go unnoticed.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0267,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "`create_order` 内の `if not inventory.reserve_stock(...)` が更新されていません。`reserve_stock` が残り在庫数を返すようになったため、最後の在庫を引き当てて残りが 0 になると戻り値 0 が falsy になります。引き当ては成功して在庫も減っているのに OutOfStock が送出され、注文は作られません。しかも在庫を減らす UPDATE が commit も rollback もされないまま残ります。判定は `is None` に直す必要があります。",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "low",
        "issue": "UPDATE と、残り数を読む SELECT が別のステートメントです。トランザクション分離の設定によっては、並行する他の注文の更新後の値を読み、返す残り数がずれる可能性があります。`RETURNING qty` を使った単一ステートメントにするほうが安全です。",
        "label": "fp"
      },
      {
        "file": "tests/test_inventory.py",
        "line": 8,
        "severity": "medium",
        "issue": "テストは、在庫 3 から 2 個引き当てて残り 1 になる場合と在庫不足の場合しか見ていません。残りがちょうど 0 になる境界(在庫 2 から 2 個引き当てる)のテストがないため、`create_order` の 0 を失敗扱いするバグを検出できません。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0127,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "`reserve_stock` now returns the remaining stock, or None on failure. `create_order` still uses `if not reserve_stock(...)`, so a successful reservation that takes stock to exactly 0 returns 0, which is falsy. `create_order` then raises OutOfStock for an order that should succeed. The stock has already been decremented by then, so the order fails while the stock is permanently lost, unless the caller rolls back. This breaks SPEC section 2 (an order that exactly empties the stock must succeed). The caller needs `is None`.",
        "label": "hit"
      },
      {
        "file": "tests/test_inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "The test does not cover the boundary where a reservation leaves exactly 0 stock. That boundary is where the falsy-0 bug appears. It also does not test `create_order` with the new return value.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0156,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round-half-to-even), not half-up. SPEC.md §1 requires 0.5 to round up. For example, discount_amount(125, 0.10) = 12.5 now returns 12 instead of 13, and 2.5 gives 2 instead of 3. Discounts are under-rounded on half cases that land on even numbers. The function name `round_half_up` is also now wrong. Float representation can also affect the result, which RESPONSE conventions say to avoid. An integer-based half-up calculation would be safer.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "The test expectation was changed from 13 to 12 to match the new implementation's output. That contradicts the spec's half-up rule (12.5 -> 13) and the review convention against copying implementation output into tests. The test now hides the regression instead of catching it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0071,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round-half-to-even), but SPEC.md requires half-up rounding for discounts. For example, 12.5 becomes 12 instead of 13, and 0.5 becomes 0 instead of 1. Discounts are therefore wrong whenever the amount lands exactly on .5, and the function name `round_half_up` no longer matches what it does. The old `int(x + 0.5)` was correct for non-negative values. The PR should keep half-up behavior, for example with `decimal.ROUND_HALF_UP` or integer arithmetic.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "The test expectation was changed from 13 to 12 to match the new implementation output. This breaks the SPEC rule that 0.5 rounds up, and REVIEW.md forbids copying implementation output into expected values. The test now asserts the behavior that violates the requirement, so it can no longer catch the regression.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0139,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "パスワードのハッシュを PBKDF2-SHA256(200,000回)から MD5(salt+password) に置き換えています。これは SPEC §3「salt 付き PBKDF2-SHA256 で保存する」に違反します。MD5 は高速なので、DB が漏えいするとオフラインで総当たりやGPU攻撃をされやすくなります。また、既存ユーザーが PBKDF2 で保存したハッシュは verify_password で MD5 と比較されるため一致せず、全員がログインできなくなります。移行処理やハッシュ方式の判別も入っていません。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0059,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "パスワードハッシュをsalt付きPBKDF2-SHA256(200,000回)から単純なMD5(salt+password)に置き換えており、SPEC §3「salt付きPBKDF2-SHA256で保存する」に違反している。MD5は高速すぎるためDB流出時にGPU等でオフラインのブルートフォースが容易になる。意図的に遅くしている処理を弱めるセキュリティ上の退行。また、既存ユーザーの保存済みPBKDF2ハッシュはMD5と一致しないため、デプロイ後は既存ユーザー全員がログインできなくなる(移行処理も無い)。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0128,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0042,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0141,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0039,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0177,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "main",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0075,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0195,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Condition uses `>` but should use `>=`. PR description states 'small total of 10000 cents or more is free' (10000セント以上なら無料), meaning >= 10000 should return 0. Currently at subtotal == 10000, the function returns 1200 instead of 0, violating the stated pricing rule.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0668,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Condition should be 'if subtotal >= 10000:' not 'if subtotal > 10000:'. The PR description states 'small subtotal が10000セント以上なら無料' (10000 cents or more is free), but the code only returns 0 when subtotal is strictly greater than 10000. This causes subtotal=10000 to incorrectly charge 1200 instead of being free.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr01",
    "rep": 3,
    "cost_usd": 0.0127,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Boundary condition is incorrect. The condition should be `if subtotal >= 10000:` to match the requirement '10000セント以上なら無料' (free at 10000 or above). Currently uses `>` instead of `>=`, so a subtotal of exactly 10000 incorrectly returns 1200 instead of 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0254,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Forbidden exception is not defined or imported. Code will raise NameError at runtime when a non-admin user calls list_all_orders. The test also expects orders.Forbidden but it's not defined in this module.",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation is wrong. Uses `page * per_page` but should use `(page - 1) * per_page` for 1-indexed pages. For page=1, per_page=10, this produces OFFSET 10 (skips first 10 items) instead of OFFSET 0 (shows items 1-10), violating the requirement '注文一覧はページ番号(1始まり)と件数で取得する。'",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0814,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation is wrong: `page * per_page` should be `(page - 1) * per_page` for 1-based page numbering. With page=1, this returns items at offset 20 instead of offset 0, skipping the first page of results.",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Undefined name 'Forbidden': the exception is raised but no import statement is visible in the diff. The test expects `orders.Forbidden` to exist. This will cause NameError at runtime if Forbidden is not imported or defined in the module.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr02",
    "rep": 3,
    "cost_usd": 0.0426,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation violates spec requirement. Formula should be (page - 1) * per_page, not page * per_page. Page 1 with per_page=20 would return offset 20 (rows 21-40) instead of offset 0 (rows 1-20), causing page numbers to be off by one.",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Forbidden exception class is not imported or defined in this module. Code raises Forbidden() but class is undefined, causing NameError at runtime. Test expects orders.Forbidden to exist.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0531,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "Calling .strip() on user[\"nickname\"] will crash with AttributeError when the nickname column is NULL/None, which occurs when users haven't set a nickname. Should use (user.get(\"nickname\") or \"\").strip() or user[\"name\"] to safely handle None values.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0317,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 6,
        "severity": "high",
        "issue": "Calling .strip() on user[\"nickname\"] will raise AttributeError if the nickname is None. The fallback logic to user[\"name\"] indicates nickname is optional, so it may be NULL in the database. Should use (user.get(\"nickname\") or \"\").strip() or user[\"name\"] instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr03",
    "rep": 3,
    "cost_usd": 0.0452,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 6,
        "severity": "high",
        "issue": "Code accesses user['nickname'] without verifying the key exists or checking for None value. Raises KeyError if the field is missing from the user dict, or AttributeError if the database returns NULL. Should use defensive access pattern like user.get('nickname', '').strip() or user['name'] to handle both cases.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0265,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition introduced between SELECT (line 6) and UPDATE (line 9). In concurrent transactions, multiple requests can read the same qty value, then both perform their updates independently, causing overbooking. Example: qty=5, two requests each reserve 3. Both SELECT qty=5, both pass the check, both UPDATE but overwrite each other, leaving qty=3 instead of failing one. This violates requirement '同時注文で在庫がマイナスになってはならない'. The original atomic UPDATE with WHERE qty >= ? correctly serialized reservations.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0202,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: SELECT and UPDATE are not atomic. Between reading stock qty and updating it, another concurrent request can read the same qty and both will reserve stock that oversells. Violates requirement 2: '同時注文で在庫がマイナスになってはならない。' The original atomic UPDATE with WHERE qty >= ? prevented this.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr04",
    "rep": 3,
    "cost_usd": 0.0167,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: SELECT and UPDATE are no longer atomic. Between line 6 (SELECT) and line 9 (UPDATE), another concurrent transaction can execute, allowing multiple requests to read the same qty value and both subtract from it. This violates SPEC requirement: '同時注文で在庫がマイナスになってはならない。' The original code used atomic UPDATE with WHERE condition, which prevented this. This PR breaks concurrent order safety.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0098,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "high",
        "issue": "Missing authorization check. The function receives 'user' parameter but never verifies the user owns the order or is an admin. Per requirement 3 (認証・認可), users should only be able to modify their own orders. An unauthenticated user could modify any order's note by calling this function.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0415,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing authorization check: The `update_order_note()` function accepts a `user` parameter but never uses it to verify that the order belongs to that user before updating. Per spec requirement 3, 'ユーザーは自分の注文だけ参照・キャンセルできる' (users can only access their own orders). The function should check `order['user_id'] == user.id` (or equivalent) before allowing the update, similar to the authorization pattern implied by the test's use of `get_order_for_user()`. This allows any authenticated user to modify any order's note.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr05",
    "rep": 3,
    "cost_usd": 0.0444,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "high",
        "issue": "Missing authorization check. The function accepts a 'user' parameter but never validates that the user owns the order or is an admin before updating. This violates SPEC.md requirement 3: 'ユーザーは自分の注文だけ参照・キャンセルできる。管理者(is_admin)は全注文を参照できる。'",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0117,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: The 'keyword' parameter is directly interpolated into the f-string without parameterization, violating the requirement 'すべてのSQLはパラメータ化する' (all SQL must be parameterized). An attacker could inject malicious SQL like keyword='%' OR '1'='1' to bypass filters or execute arbitrary commands.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0128,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: uses f-string formatting instead of parameterized queries. The user_id and keyword are interpolated directly into the SQL string, violating the requirement 'すべてのSQLはパラメータ化する。' (All SQL must be parameterized). Should use: sql = \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\" with parameters (user_id, f'%{keyword}%')",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr06",
    "rep": 3,
    "cost_usd": 0.0342,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: 'keyword' parameter is directly interpolated into SQL using f-string. Violates requirement 'すべてのSQLはパラメータ化する。' (All SQL must be parameterized). Should use: \"SELECT * FROM orders WHERE user_id = ? AND note LIKE ? ORDER BY id\" with parameters (user_id, f'%{keyword}%')",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0323,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "Hardcoded API key violates requirement 3. Requirement states: 'secret information (API keys, etc.) should not be included in the repository, but should be read from environment variables'. Use os.environ.get('MAIL_API_KEY') instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0107,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "Hardcoded API key in repository violates Requirement 3: '秘密情報(APIキー等)はリポジトリに含めず環境変数から読む' (Secret information like API keys must not be in the repository and should be read from environment variables). Should use os.environ.get('MAIL_API_KEY') like DB_PATH does.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr07",
    "rep": 3,
    "cost_usd": 0.0102,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 8,
        "severity": "high",
        "issue": "Hardcoded API key in repository. SPEC.md requirement #3 states: 秘密情報(APIキー等)はリポジトリに含めず環境変数から読む (Secret information like API keys must not be included in the repository and must be read from environment variables). MAIL_API_KEY should be: os.environ.get(\"MAIL_API_KEY\") with appropriate default or error handling.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0639,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "Violates spec requirement: The implementation applies both bulk discount (line 48) and coupon discount (line 49), but spec requires: 'クーポンはまとめ買い割引と併用できない' (coupons and bulk discounts cannot be combined). Only the larger discount amount should be applied.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0567,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "Function applies both bulk discount (line 48) and coupon discount unconditionally, violating SPEC.md section 1: 'クーポンはまとめ買い割引と併用できない。両方該当する場合は割引額が大きい方だけを適用する' (Coupon and bulk discount cannot be used together; apply only the larger discount). Current implementation will overcharge when both bulk and coupon discounts apply to an order. Test case happens to pass because qty=2 has no bulk discount, masking the bug.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr08",
    "rep": 3,
    "cost_usd": 0.079,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 48,
        "severity": "high",
        "issue": "Function applies both bulk discount (line 48) and coupon (line 49) to the same order, violating specification requirement: 'クーポンはまとめ買い割引と併用できない' (coupons and bulk discounts are mutually exclusive). Spec states only the larger discount amount should apply when both conditions are met. Current implementation would incorrectly double-discount when qty≥10.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0218,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Violates SPEC.md requirement: '注文をキャンセルできるのは status が \"pending\" の注文だけ'. The condition `if order[\"status\"] == \"cancelled\"` allows cancelling orders in any state except 'cancelled' (e.g., 'shipped', 'delivered'). Should be `if order[\"status\"] != \"pending\"` to enforce the specification.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0228,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The condition only checks if status is 'cancelled', but SPEC.md section 4 requires 'only pending orders can be cancelled'. This change allows cancelling orders with status 'shipped', 'delivered', or any non-cancelled status, violating the specification.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr09",
    "rep": 3,
    "cost_usd": 0.0228,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The condition only prevents cancellation when status is 'cancelled', but allows cancellation of any other status. This violates SPEC.md Section 4: 'キャンセルできるのは status が \"pending\" の注文だけ' (only pending orders can be cancelled). Orders with status 'shipped', 'completed', etc. would now be cancellable, breaking the specification.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0518,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "medium",
        "issue": "fetchone() may return None if the stock record is deleted between UPDATE and SELECT (race condition), causing a crash when accessing [0]. Should add null checking before indexing.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0472,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 11,
        "severity": "medium",
        "issue": "Unsafe access to fetchone() result without null check - if fetchone() returns None, this will raise TypeError when accessing [0]",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr10",
    "rep": 3,
    "cost_usd": 0.0386,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0154,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Python's round() uses 'round half to even' (banker's rounding), not 'half-up' as required by spec. For 12.5, round(12.5)=12, but spec requires 0.5→UP (13). This violates the requirement: '割引額の端数は四捨五入(0.5は切り上げ = half up)で整数セントにする。'",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation is wrong. For discount_amount(125, 0.10): 125*0.1=12.5 must round UP to 13 per spec, not 12. Changing test to match incorrect behavior hides the bug.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0105,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Requirement violation: SPEC.md requires half-up rounding (0.5 rounds up), but Python's round() uses banker's rounding (half to even). For example, round(12.5) returns 12, not 13. This breaks the discount calculation requirement.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation violates SPEC.md requirement. The test expects 12.5 to round to 12, but the requirement explicitly states '0.5は切り上げ' (0.5 rounds up), so 12.5 should round to 13.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr11",
    "rep": 3,
    "cost_usd": 0.0646,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "round_half_up() changed from int(x + 0.5) to round(x), which implements banker's rounding (round half to even), not 'half up' rounding. This violates SPEC.md: '割引額の端数は四捨五入(0.5は切り上げ = half up)で整数セントにする' (discount amounts must use half up rounding where 0.5 rounds up). Example: round(12.5) returns 12 instead of required 13.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "Test expectation changed from 13 to 12 to match incorrect banker's rounding behavior. SPEC.md requires discount_amount(125, 0.10) to round 12.5 up to 13 (half up rounding), not down to 12. Test now validates behavior that violates requirements.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0084,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Violates SPEC.md requirement 3 (認証・認可): 'パスワードは salt 付き PBKDF2-SHA256 で保存する' (Passwords must be stored using PBKDF2-SHA256 with salt). MD5 is not PBKDF2-SHA256.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Critical security vulnerability: MD5 is cryptographically broken and unsuitable for password hashing. It provides no protection against brute-force attacks. PBKDF2 with 200,000 iterations intentionally slows verification to resist attacks; MD5 is fast, which is a vulnerability. This breaks password security for all users.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0088,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Violates SPEC.md requirement: 'パスワードは salt 付き PBKDF2-SHA256 で保存する' (passwords must be stored using PBKDF2-SHA256 with salt). This change replaces it with MD5.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Security vulnerability: MD5 is cryptographically broken and unsuitable for password hashing. It offers no protection against brute-force attacks compared to PBKDF2's 200,000 iterations.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Backwards compatibility break: Existing passwords hashed with PBKDF2-SHA256 will fail verification because verify_password() calls the new MD5-based hash_password(), which produces different output for the same input.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr12",
    "rep": 3,
    "cost_usd": 0.0085,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "SPEC.md section 3 requires 'salt 付き PBKDF2-SHA256' for password storage. MD5 violates this requirement and is cryptographically broken for password hashing: it lacks resistance to brute-force attacks, has collision vulnerabilities, and is too fast. PBKDF2 with 200,000 iterations is the specified security requirement, not a performance bottleneck to remove.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0152,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0293,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr13",
    "rep": 3,
    "cost_usd": 0.0308,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0355,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0446,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr14",
    "rep": 3,
    "cost_usd": 0.026,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0337,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0514,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "S",
    "pr": "pr15",
    "rep": 3,
    "cost_usd": 0.03,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.041,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Condition should use >= instead of >. According to requirement '10000セント以上', subtotal of exactly 10000 should return 0 (free), but current code returns 1200.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0342,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Condition should be `>=` not `>`. Requirement states '10000セント以上' (10000 or more), but code only applies free shipping for amounts strictly greater than 10000. When subtotal equals exactly 10000, it incorrectly returns 1200 instead of 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr01",
    "rep": 3,
    "cost_usd": 0.0293,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "high",
        "issue": "Boundary condition is wrong: should be 'if subtotal >= 10000:' not 'if subtotal > 10000:'. The PR description states shipping is free for 10000 or more, but the code only makes it free for amounts strictly greater than 10000, meaning exactly 10000 would incorrectly be charged 1200.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0389,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Incorrect offset calculation for pagination. Should be (page - 1) * per_page, not page * per_page. This causes page 1 to skip the first per_page items and all subsequent pages to be misaligned. Inconsistent with list_my_orders on line 36 which uses the correct formula.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0248,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset is incorrect. Should use (page - 1) * per_page instead of page * per_page to match list_my_orders logic and return results starting from page 1. Current code skips first page and returns page 2 onwards.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr02",
    "rep": 3,
    "cost_usd": 0.0201,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Pagination offset calculation is wrong. Should be (page - 1) * per_page, not page * per_page. This causes page=1 to skip the first batch of results and return the second batch instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0325,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "Calls .strip() on user['nickname'] without null check; raises AttributeError when nickname is None (nullable column in schema)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0495,
    "turns": 13,
    "tool_calls": 12,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "Calling .strip() on user[\"nickname\"] crashes with AttributeError when nickname is None (nullable in database)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr03",
    "rep": 3,
    "cost_usd": 0.0277,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "user[\"nickname\"] can be NULL from database; .strip() called without null check raises AttributeError",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0491,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: Splitting SELECT and UPDATE into separate operations destroys atomicity. Multiple concurrent transactions can reserve the same inventory. Original atomic UPDATE with WHERE condition must be preserved to prevent overselling.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.065,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: splitting atomic UPDATE into SELECT+UPDATE allows concurrent transactions to both read sufficient inventory and both succeed, causing inventory overselling. The old code's atomic UPDATE with WHERE clause prevented this.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr04",
    "rep": 3,
    "cost_usd": 0.0837,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "Race condition: splitting atomic UPDATE into separate SELECT+UPDATE creates a window where concurrent requests can both read the same qty value, then both update it, causing inventory to be inconsistently decremented. Original atomic UPDATE with WHERE condition was safer for concurrent access.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0407,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Missing user authorization check. Should use get_order_for_user(conn, user, order_id) instead of db.get_order(conn, order_id) to ensure only the order owner or admin can update the note, consistent with cancel_order function",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0266,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "Authorization bypass: uses db.get_order instead of get_order_for_user, allowing any user to modify notes on any order, not just their own",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr05",
    "rep": 3,
    "cost_usd": 0.0295,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 48,
        "severity": "high",
        "issue": "Missing authorization check: update_order_note should use get_order_for_user() like cancel_order does to verify the user owns the order or is an admin, instead of calling db.get_order() directly",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0291,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is directly interpolated into SQL string without parameterized query. Should use ? placeholder and pass f\"%{keyword}%\" as a parameter tuple value instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0418,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is interpolated directly into SQL string without parameterization. Should use ? placeholder with tuple parameter like all other database functions in this file.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr06",
    "rep": 3,
    "cost_usd": 0.039,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection vulnerability: keyword parameter is directly interpolated into SQL string without parameterization. Attacker can inject arbitrary SQL code.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0254,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "MAIL_API_KEY is hardcoded with what appears to be a real API key, violating the file's stated policy to read secret information from environment variables. Should use os.environ.get()",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.035,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "Hardcoded API key violates project design principle ('秘密情報は環境変数から読む。'). API keys must be read from environment variables, not hardcoded in source code, as they will be exposed in git history and accessible to all repository users.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr07",
    "rep": 3,
    "cost_usd": 0.0338,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "Hardcoded API key in source code violates project's documented security principle of reading secrets from environment variables. API keys must never be committed to version control.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0372,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0847,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr08",
    "rep": 3,
    "cost_usd": 0.066,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0411,
    "turns": 12,
    "tool_calls": 11,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Allows cancelling non-pending orders (completed, shipped, etc.), violating the requirement that 'only pending orders can be cancelled'",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0375,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Changed from 'status != pending' to 'status == cancelled', allowing cancellation of non-pending orders and violating the requirement that only pending orders can be cancelled",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr09",
    "rep": 3,
    "cost_usd": 0.0317,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "Order status check changed from 'status != pending' to 'status == cancelled', violating the specification that only pending orders can be cancelled. The code now allows cancelling orders with any status except 'cancelled'.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.036,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "Breaking change: reserve_stock now returns int|None instead of bool. When remaining stock is 0, 'if not 0:' evaluates to True, incorrectly raising OutOfStock on successful reservations",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0369,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "Logic error: returning 0 for remaining stock when the last item is reserved will incorrectly raise OutOfStock exception. Must change to 'if inventory.reserve_stock(...) is None:' to properly handle the new int|None return type",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr10",
    "rep": 3,
    "cost_usd": 0.0305,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "high",
        "issue": "Breaking API change: return type changed from bool to int|None. Existing caller in orders.py (line 18) uses 'if not' to check for failure, which will incorrectly fail when remaining stock is 0 (zero is falsy in Python)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0457,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function named 'round_half_up' now uses banker's rounding (round to even) instead of rounding 0.5 upward. This breaks the semantic contract implied by the function name. Example: 12.5 now rounds to 12 instead of 13. In pricing calculations, this behavioral change could produce incorrect results.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0415,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function named 'round_half_up' no longer implements round-half-up semantics. Changed from 'int(x + 0.5)' (always rounds 0.5 up) to 'round(x)' (banker's rounding, rounds 0.5 to nearest even). In financial/pricing code, this semantic change can cause incorrect discount calculations and is misleading given the function name.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr11",
    "rep": 3,
    "cost_usd": 0.0426,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Function named 'round_half_up' no longer implements round-half-up. Python's round() uses banker's rounding (round-half-to-even), which produces different results for .5 values. Example: 12.5 becomes 12 instead of 13. This breaks the function's contract and changes financial calculations. Should either rename the function or restore the correct implementation.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Test expectation changed from 13 to 12 to match the new (incorrect) rounding behavior. This validates the wrong behavior instead of round-half-up. The test should verify that 12.5 rounds to 13, not 12.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0272,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "MD5 is cryptographically broken and unsuitable for password hashing. Replaces secure PBKDF2-HMAC with 200,000 iterations with insecure MD5 with no iterations, making passwords vulnerable to brute-force attacks. Rainbow tables for MD5 are widely available. Password hashing should prioritize security over login speed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0218,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "MD5 is cryptographically broken and unsuitable for password hashing. Switching from PBKDF2-HMAC-SHA256 with 200,000 iterations to MD5 dramatically reduces security: MD5 has known collision vulnerabilities, is extremely fast (enabling efficient brute-force attacks), and was deprecated for security purposes decades ago. This change makes password cracking trivially easy and exposes all users to compromise. Use argon2, scrypt, or bcrypt instead.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr12",
    "rep": 3,
    "cost_usd": 0.0245,
    "turns": 5,
    "tool_calls": 4,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "MD5 is cryptographically broken and unsuitable for password hashing. The original PBKDF2-HMAC-SHA256 with 200,000 iterations was intentionally slow to provide brute-force resistance. Replacing it with MD5 makes passwords vulnerable to fast brute-force attacks and violates security best practices (NIST, OWASP). Attackers can crack passwords at millions of attempts per second. Use bcrypt, scrypt, or Argon2 instead if performance optimization is needed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0478,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0566,
    "turns": 11,
    "tool_calls": 10,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr13",
    "rep": 3,
    "cost_usd": 0.0527,
    "turns": 9,
    "tool_calls": 8,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0824,
    "turns": 10,
    "tool_calls": 9,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.108,
    "turns": 14,
    "tool_calls": 13,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr14",
    "rep": 3,
    "cost_usd": 0.0352,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0513,
    "turns": 7,
    "tool_calls": 6,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0417,
    "turns": 6,
    "tool_calls": 5,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "haiku",
    "cond": "T",
    "pr": "pr15",
    "rep": 3,
    "cost_usd": 0.0417,
    "turns": 8,
    "tool_calls": 7,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0137,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0037,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0246,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "medium",
        "issue": "Off-by-one pagination. `page` is 1-based (default 1), but the offset is computed as `page * per_page`. As a result, page=1 skips the first `per_page` orders and they can never be returned. For example, with the defaults (page=1, per_page=20) the offset is 20, so orders 1-20 are unreachable. The offset should be `(page - 1) * per_page`. Negative or zero `page` values are also not validated.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 45,
        "severity": "low",
        "issue": "The test only checks `0 < len(page1) <= 10`. With 25 orders, page=1 wrongly uses offset 10 and still returns 10 rows, so the test passes and hides the pagination bug. It should assert which order IDs are returned, for example that the first page starts at the first order.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0112,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Off-by-one in pagination: `page` is 1-based (default 1), but the offset is computed as `page * per_page`. Page 1 therefore starts at row per_page, so the first page of orders is always skipped and can never be returned. It should be `(page - 1) * per_page`. Page 0 would return the first page, and negative pages produce a negative offset.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 45,
        "severity": "low",
        "issue": "The assertion `0 < len(page1) <= 10` is too weak to catch the offset bug. With 25 orders, page 1 still returns 10 rows (rows 11-20), so the test passes even though the first 10 orders are skipped. It should assert which orders are returned, for example their ids.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.026,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "`user[\"nickname\"].strip()` assumes every user has a non-null nickname. If the column is NULL or the key is missing (users without a nickname, or a schema/row dict without that field), `order_summary` raises AttributeError or KeyError instead of falling back to `user[\"name\"]`. Guard it, e.g. `(user.get(\"nickname\") or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The assertion `\"Al\" in summary` is a substring of the fallback name \"Alice\", so the test also passes when the nickname is ignored. It does not check that the nickname is used. It also does not cover the cases most likely to break: an empty or whitespace-only nickname, or a NULL nickname.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0114,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 9,
        "severity": "high",
        "issue": "`user[\"nickname\"].strip()` crashes with AttributeError when nickname is NULL/None, which is likely for users who never set one, so the order summary fails for them. It also raises KeyError/IndexError if the user row or dict has no `nickname` column. Guard with `(user.get(\"nickname\") or \"\").strip() or user[\"name\"]` or similar.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "Weak assertion: `\"Al\" in summary` also passes when the full name 'Alice' is shown, so the test does not prove the nickname is used. It also has no case for a missing, None, or blank nickname, which would have caught the crash above.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.016,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "high",
        "issue": "Check-then-act race: replacing the atomic conditional UPDATE with SELECT followed by an absolute-value UPDATE (qty = row_qty - qty) lets concurrent reservations both read the same qty and both succeed, overselling stock and causing lost updates (one decrement overwrites another). The original single atomic statement prevented this.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "row[\"qty\"] assumes the connection uses a dict-like row factory (e.g. sqlite3.Row); with default tuple rows this raises TypeError. The previous code had no such dependency.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0067,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 6,
        "severity": "high",
        "issue": "The new code reads stock with a SELECT and then writes it with an UPDATE in a separate step, so the old atomic conditional UPDATE is gone. When two reservations run at the same time, both can read the same qty and both pass the check. The second write then overwrites the first (lost update), which oversells stock, or one reservation is silently dropped while still returning True.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "row[\"qty\"] only works if conn uses a dict-like row_factory such as sqlite3.Row. With the default tuple rows it raises TypeError, whereas the old code did not depend on the row factory.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0218,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to `user` (unlike cancel_order / get_order_for_user, which use the user). Any authenticated user can overwrite the note on any other user's order by guessing/enumerating order IDs (IDOR). The `user` parameter is accepted but unused.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "medium",
        "issue": "set_order_note writes to an `orders.note` column; nothing in the diff adds a migration/schema change for this column. If the existing schema lacks `note`, the UPDATE fails with 'no such column: note' at runtime (and the new test fails).",
        "label": "fp"
      },
      {
        "file": "shop/orders.py",
        "line": 52,
        "severity": "low",
        "issue": "Note is silently truncated to 200 characters with no validation error, and `note` is not type/None-checked (passing None raises TypeError on slicing). Silent data loss for longer notes.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0102,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note fetches the order with db.get_order and never checks that it belongs to `user`, even though `user` is passed in. Any authenticated user can overwrite the note on another user's order by supplying its id (IDOR). Use the ownership-checked lookup (e.g. get_order_for_user, as the test does) or compare order['user_id'] to user.id and raise NotFound or Forbidden.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "low",
        "issue": "The test only covers the owner updating their own note. It does not check that another user is rejected or that notes are truncated at 200 characters, so the missing authorization check goes undetected.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0203,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: keyword is interpolated directly into the SQL string. A keyword like \"x' OR 1=1 --\" bypasses the user_id filter and returns other users' orders, and crafted input can run arbitrary SQL. Use a parameterized query instead: \"... WHERE user_id = ? AND note LIKE ? ...\" with ('%' + keyword + '%').",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "low",
        "issue": "LIKE wildcards in the keyword (% and _) are not escaped, so a search for '%' matches every order. Also, a keyword that contains a single quote causes a syntax error even for legitimate input.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0079,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string. A keyword like `' OR 1=1 --` drops the user_id filter and exposes every user's orders, and other payloads can run arbitrary SQL. Any keyword containing a single quote also breaks the query. Use a parameterized query instead: `WHERE user_id = ? AND note LIKE ?` with `('%' + keyword + '%',)`.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "low",
        "issue": "LIKE wildcards are not escaped. `%` and `_` in the user's keyword act as wildcards, so a keyword like `%` or `_` matches all of the user's orders instead of matching literally. Escape them and add an ESCAPE clause.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0209,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "A live mail API secret key (sk_live_...) is hardcoded in the source and committed to the repository. Anyone with repo access can use it. It should come from an environment variable or a secret store, and the leaked key should be revoked and rotated.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 14,
        "severity": "medium",
        "issue": "urlopen raises HTTPError/URLError/timeout on non-2xx responses or network failure. send_order_confirmation does not catch these, so a mail outage or timeout can propagate to the caller and break order processing. The return value says it returns a status code, but failures actually raise exceptions.",
        "label": "valid"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The amount is shown to the customer as raw cents, e.g. '合計 900 セント'. It is not formatted as a currency amount, which makes the confirmation email confusing.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0106,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "A live mail API secret key (sk_live_...) is hardcoded in the source and committed to the repo. Anyone with read access to the repository or its history can use the key, and removing it in a later commit does not purge it from history. Load it from an environment variable or a secret store (e.g. os.environ[\"MAIL_API_KEY\"]) and revoke/rotate the leaked key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "total is presumably an integer in the smallest currency unit, but it is inserted into the body as-is as \"合計 {total} セント\". Customers see values like \"合計 900 セント\", which do not match the expected display format (e.g. a currency-formatted amount like $9.00). Use the same amount formatting as the rest of the app.",
        "label": "fp"
      },
      {
        "file": "shop/mailer.py",
        "line": 14,
        "severity": "low",
        "issue": "urlopen raises HTTPError/URLError/timeout on non-2xx responses or network failures, and these propagate to the caller. If this is called inside the order-confirmation flow, a failed send can make the order processing itself fail. Catch the exception and log it, or make the send async/retryable.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0217,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "low",
        "issue": "coupon_percent is not validated. A value over 100 makes the subtotal negative and returns a negative or nonsensical total. A negative value increases the price. Reject inputs outside 0–100.",
        "label": "valid"
      },
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "low",
        "issue": "The per-line bulk-discount loop is copied from order_total instead of shared. If the bulk-discount logic changes in one function, the two totals will diverge.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0106,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "medium",
        "issue": "`coupon_percent` is never validated. A value over 100 makes the subtotal negative, so the order total can drop below the shipping fee or go negative. A negative value raises the price instead of discounting it. Reject or clamp values outside 0–100.",
        "label": "valid"
      },
      {
        "file": "shop/pricing.py",
        "line": 47,
        "severity": "low",
        "issue": "The per-line bulk-discount loop is copied from `order_total`. If the bulk-discount logic changes in one function and not the other, coupon and non-coupon totals will disagree. Factor the discounted subtotal into a shared helper.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0219,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The guard used to allow cancelling only 'pending' orders. Now it only rejects 'cancelled', so shipped, delivered, refunded or any other non-pending order can be cancelled. Cancelling one also calls inventory.release_stock, which adds stock back for goods that have already left the warehouse. This inflates inventory and can lead to overselling. The old restriction is gone and no replacement check for non-cancellable states was added.",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "medium",
        "issue": "The status check and the update are separate steps with no lock and no conditional update (e.g. UPDATE ... WHERE status != 'cancelled'). Two concurrent cancel requests can both pass the check, and each then calls release_stock, so the stock is released twice. The new 'already cancelled' error only covers sequential calls.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0073,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 42,
        "severity": "high",
        "issue": "The guard used to allow cancelling only 'pending' orders. It now rejects only 'cancelled' ones, so orders that are shipped, delivered or in any other non-pending state can be cancelled. Cancelling one of those calls inventory.release_stock and adds stock back for goods that have already left the warehouse, which inflates inventory and allows overselling. It also lets users cancel orders that have already been fulfilled.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0235,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "high",
        "issue": "The return type changed from bool to int | None. When a reservation uses up the last unit, the function returns 0, which is falsy. Any caller written against the old bool contract (`if reserve_stock(...)` or `if not reserve_stock(...)`) will read that as a failure, even though the stock was already decremented. Those units are then never released or completed. Callers checking `== True` / `is True` will treat every successful reservation as a failure.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "medium",
        "issue": "This is a breaking change to a public function's contract, but no call sites are updated. Every existing caller that relies on the bool return (`if not reserve_stock(...): ...`) has to switch to an `is None` check, or the success/failure logic will be wrong.",
        "label": "partial"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0126,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "high",
        "issue": "The return type changed from bool to int | None. Existing callers that check truthiness (`if reserve_stock(...)` or `if not reserve_stock(...)`) will treat a successful reservation that leaves 0 in stock as a failure, because 0 is falsy. When the last units are sold, the stock is already decremented but the caller acts as if the reservation failed: the order is rejected and the stock is leaked, or a compensating path is skipped. Callers must compare against None explicitly; this PR does not update them.",
        "label": "hit"
      },
      {
        "file": "tests/test_inventory.py",
        "line": 7,
        "severity": "low",
        "issue": "The test does not cover the case where a reservation succeeds and leaves exactly 0 in stock. That is the edge case where the new int return value differs in truthiness from the old True, so the regression above goes undetected.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0171,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "round_half_up now uses Python's built-in round(), which does banker's rounding (round-half-to-even), not half-up. Every discount that lands exactly on .5 rounds to the even neighbour instead of up, e.g. 12.5 -> 12, while 13.5 still -> 14. The function name no longer matches its behaviour and discounts are understated by 1 on half-even cases. Floating-point inputs like 2.675 also round unpredictably.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "medium",
        "issue": "The test expectation was changed to 12 to match the new banker's rounding instead of catching the regression. The test is still named test_round_half_up but now asserts round-half-to-even behaviour (12.5 -> 12), so it locks in the bug.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0074,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "round_half_up now calls Python's round(), which does banker's rounding (round-half-to-even), not half-up. Values ending in exactly .5 now round to the nearest even integer: 12.5 -> 12, 0.5 -> 0, 2.5 -> 2. Discounts on those totals come out lower than before and no longer match the function's name or contract. Float values such as 12.4999... from total*rate also behave differently.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "medium",
        "issue": "The test was changed to expect 12 for 12.5 so it matches the new banker's-rounding result. It no longer checks half-up rounding, which hides the regression; the test's name and comment ('round half up') now contradict what it asserts.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0151,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Password hashing was downgraded from PBKDF2-HMAC-SHA256 with 200,000 iterations to a single unsalted-iteration MD5 over salt+password. MD5 is fast and cryptographically broken, so leaked hashes can be brute-forced at billions of guesses per second. Use PBKDF2/scrypt/bcrypt/argon2 instead.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Changing the hash algorithm without a migration breaks every existing user. Stored hashes were produced with PBKDF2, so verify_password now computes MD5 for the same salt, the digests never match, and all existing users can no longer log in.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0071,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Password hashing is downgraded from PBKDF2-HMAC-SHA256 with 200,000 iterations to a single unsalted-construction MD5 digest. MD5 is fast and cryptographically broken, so leaked hashes can be brute-forced at billions of guesses per second. Use a slow KDF such as pbkdf2_hmac, scrypt, bcrypt, or argon2.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Breaking change for existing users: hashes already stored with PBKDF2 (32-byte digests) will never match the new 16-byte MD5 output, so verify_password returns False and every existing user is locked out. No migration or fallback path is provided.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.011,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0017,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0159,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0039,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0171,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "A",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0045,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0439,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "Off-by-one at the free-shipping threshold. The PR description says a subtotal of 10000 cents or more ships free, matching the 'or more' (以上) rule SPEC.md uses for regular shipping. The code uses `subtotal > 10000`, so a subtotal of exactly 10000 is charged 1200 cents. It should be `>= 10000`. The new tests check only 9999 and 20000, so they never hit the boundary and miss this.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0088,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "Off-by-one at the free-shipping threshold. The PR description says shipping is free when the subtotal is 10000 cents or more, and SPEC uses 'or more' for its own free-shipping threshold. The code checks `subtotal > 10000`, so a subtotal of exactly 10000 is charged 1200 instead of 0. It should be `>= 10000`.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 26,
        "severity": "low",
        "issue": "The tests only check 9999 and 20000 and skip the boundary value 10000, so they don't catch the off-by-one in the threshold check.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0589,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Off-by-one in the paging offset. It is computed as page * per_page, but pages start at 1 (per the spec and PR description), so it should be (page - 1) * per_page, as list_my_orders already does. With page=1 the first per_page orders are skipped and page 1 actually returns page 2. If there are per_page orders or fewer, page 1 comes back empty.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The assertion `0 < len(page1) <= 10` is too loose to catch the offset bug. With 25 orders, page 1 incorrectly returns orders 11-20, which is still 10 rows, so the test passes. It should check the exact contents, e.g. that page 1 starts with the first order ID.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0195,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "The offset is calculated as `page * per_page`, but `page` starts at 1 (per SPEC §4 and the PR description). So page=1 skips the first `per_page` orders and returns what should be page 2, and the first page of orders can never be fetched. The existing `list_my_orders` correctly uses `(page - 1) * per_page`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The assertion `0 < len(page1) <= 10` is too loose to catch the off-by-one page bug. With 25+ orders, page=1 returns orders 11-20 and the test still passes. It should check the exact IDs or check that the first order is included.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0406,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "The `users.nickname` column can be NULL: `shop/db.py` declares it as `nickname TEXT` with no NOT NULL, and the test fixture inserts bob with a NULL nickname. For any such user, `user[\"nickname\"]` is None, so `.strip()` raises AttributeError and `order_summary` crashes. That breaks summaries for every user without a nickname, including all users created without one, as in `tests/test_auth.py`. Use something like `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The new test only covers a user who has a nickname. It never checks the fallback to the username when the nickname is NULL or empty (for example, bob), which is exactly the case that crashes. The `\"Al\" in ...` substring check is also weak and could pass by accident.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0256,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "The users.nickname column can be NULL (the schema has no NOT NULL, and the test inserts bob with nickname NULL). When nickname is NULL, user[\"nickname\"] is None, so calling .strip() raises AttributeError and order_summary crashes. The fallback to name only handles an empty string, not NULL. Use (user[\"nickname\"] or \"\").strip() or user[\"name\"].",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The test only covers a user who has a nickname (alice). It never checks a user whose nickname is NULL or empty (bob), so the crash above goes uncaught. The \"Al\" substring check is also weak: it would pass for unrelated text, such as a name containing \"Al\".",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0316,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "This change replaces one atomic conditional UPDATE (`qty = qty - ? WHERE qty >= ?`) with a SELECT followed by an UPDATE that writes an absolute value. That creates a check-then-act race. Two concurrent orders can both read the same qty and both pass the check. Each then writes `row_qty - qty`, so one decrement is lost. Both orders succeed and stock is oversold beyond what exists, which violates SPEC §2 (concurrent orders must not drive stock negative). Nothing opens an explicit transaction (BEGIN IMMEDIATE) or takes a lock to prevent this. So the PR's claim that behaviour is unchanged is wrong under concurrency. Fix: go back to the atomic conditional UPDATE and check rowcount.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0126,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 9,
        "severity": "high",
        "issue": "This adds a check-then-act race. The code reads qty with SELECT, checks it in Python, then writes back an absolute value (row['qty'] - qty) with no guard and no lock. Two orders running at the same time can both read the same qty and both pass the check. Then one update overwrites the other (a lost update), and stock can be oversold. The old single UPDATE ... WHERE qty >= ? was atomic and prevented this. This breaks SPEC §2 (stock must not go negative under concurrent orders), and the PR description is wrong to say behavior is unchanged.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0439,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note looks up the order with db.get_order but never checks who owns it. Any logged-in user can overwrite the note on another user's order just by passing its order_id. This breaks SPEC 3, which says users can only access their own orders. Use get_order_for_user, as cancel_order does, so a Forbidden error is raised for orders the user doesn't own.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0256,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note never checks who owns the order. It calls db.get_order directly instead of get_order_for_user, so any logged-in user can overwrite the note on someone else's order just by knowing its ID. This breaks the SPEC rule that users may only access their own orders. Use get_order_for_user, which raises Forbidden for other users' orders.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0326,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `search_orders` puts `keyword` straight into the SQL with an f-string. This breaks the spec rule that all SQL must be parameterized. A keyword such as `' OR 1=1 OR note LIKE '` gets past the `user_id` filter and returns other users' orders, which breaks the rule that users can only see their own orders. A UNION-based payload can also read other tables (users, password hashes). Even a harmless note containing a single quote makes the query fail. Fix: use `LIKE ?` with the parameter `('%' + keyword + '%',)`, and put `user_id` in a parameter too.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "low",
        "issue": "`%` and `_` in the keyword are not escaped, so the search is not a true substring match. For example, searching for `_` matches every note. Escape these characters and add an `ESCAPE` clause.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0094,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 56,
        "severity": "high",
        "issue": "SQL injection. The keyword goes straight into the SQL string through an f-string. A keyword like `' OR 1=1 --` escapes the LIKE clause and the user_id filter, which exposes other users' orders. Stacked or crafted payloads could do more. This breaks the spec rules that all SQL must be parameterized and that users can only see their own orders. Use `... WHERE user_id = ? AND note LIKE ?` with `('%' + keyword + '%')` as a bound parameter.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 56,
        "severity": "low",
        "issue": "LIKE wildcards are not escaped. A keyword containing `%` or `_` acts as a pattern instead of a literal, so the partial-match results are wrong. Escape these characters and add an `ESCAPE` clause.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0241,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "A live mail API secret key (sk_live_...) is hardcoded and committed to the repository. This violates SPEC 3, which says secrets such as API keys must not be in the repository and must be read from environment variables. Anyone with repo access can use the key, and it stays in git history. Read it from an environment variable (e.g. os.environ['MAIL_API_KEY']) and rotate the leaked key.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0084,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 10,
        "severity": "high",
        "issue": "A live API secret (sk_live_...) is hardcoded and committed to the repository. This violates SPEC §3, which says secrets must not be in the repo and must be read from environment variables. Anyone with repo access can read and misuse the key, so it must be revoked and rotated. Read it with os.environ (e.g. os.environ['MAIL_API_KEY']) instead.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 14,
        "severity": "low",
        "issue": "urlopen raises HTTPError/URLError on non-2xx responses or network failures, and nothing catches them. If this runs when an order completes, a mail outage would make the order flow fail with an exception.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0352,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "The coupon is applied on top of the subtotal after the bulk discount, so both discounts stack. SPEC §1 says a coupon cannot be combined with the bulk discount: when both apply, only the larger discount should be used. Example: [(1000, 10)] with a 10% coupon gives 10000→9000→8100 here, but the spec result is 9000 (the larger discount is 1000, applied once). The test only covers qty=2, where no bulk discount applies, so it misses this.",
        "label": "hit"
      },
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "low",
        "issue": "coupon_percent is not validated. A negative value or a value over 100 gives a surcharge or a negative subtotal.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0097,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "The coupon is applied on top of the subtotal that already has the bulk-purchase discount taken off, so both discounts stack. SPEC §1 says a coupon cannot be combined with the bulk discount; when both apply, only the larger one should be used. Example: [(1000, 10)] with a 10% coupon gives 9000 - 900 = 8100 instead of 9000. The new test only covers qty=2, where no bulk discount applies, so it doesn't catch this.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0266,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "This breaks the SPEC rule that only orders with status \"pending\" can be cancelled, even though the PR description says that rule is unchanged. The `status != \"pending\"` check is replaced with `status == \"cancelled\"`, so orders in any other non-pending status (such as shipped or completed) can now be cancelled. When that happens, `release_stock` also adds their quantity back to inventory, which inflates stock. The fix is to keep the pending check and raise the clearer 'already cancelled' error only for status \"cancelled\".",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0103,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The check changed from `status != \"pending\"` to `status == \"cancelled\"`. Orders in any other non-pending status (for example shipped, paid or completed) can now be cancelled, and their stock is released back to inventory. This breaks SPEC §4, which says only \"pending\" orders can be cancelled, and contradicts the PR description's claim that the rule is unchanged. It also lets inventory be inflated by cancelling already-fulfilled orders. Fix: keep the pending check and raise \"already cancelled\" only as a more specific message for the cancelled case before it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0393,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "`reserve_stock` now returns the remaining stock count (an int) instead of a bool, but `create_order` still checks it with `if not inventory.reserve_stock(...)`. When an order uses up the last of the stock, the function returns 0, which counts as false. The order then fails with OutOfStock even though there was enough stock. Worse, the UPDATE has already lowered the stock and nothing restores it, so stock is lost. This breaks SPEC §2: when an order fails, stock must not change. The check should be `is None`.",
        "label": "hit"
      },
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "low",
        "issue": "The remaining count is read with a separate SELECT after the UPDATE. If another order reserves the same SKU in between, the returned value is wrong for this reservation. The count could come from the UPDATE itself with `UPDATE ... RETURNING qty`, or both statements could run in one transaction.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0197,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "reserve_stock now returns the remaining stock count instead of a bool, but this caller still checks `if not inventory.reserve_stock(...)`. When an order uses up the last of the stock, reserve_stock returns 0, which is falsy. create_order then raises OutOfStock even though the stock was already decremented. The order fails and those units are lost, which violates SPEC §2 (a failed order must not change stock). Example: stock 2, order qty 2 -> OutOfStock and stock becomes 0. The check should be `is None`.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0237,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round half to even), so round(12.5) == 12 and round(0.5) == 0. SPEC requires half-up rounding for discount fractions (0.5 rounds up). Any discount that ends in exactly .5 cents is now rounded down whenever the integer part is even, which under-discounts orders (e.g. 125 * 0.10 -> 12 instead of 13). The function name round_half_up no longer matches what it does.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "medium",
        "issue": "The test's expected value was changed from 13 to 12 to match the new banker's rounding. That contradicts the SPEC's half-up rule (12.5 -> 13), so the test now locks in the requirement violation instead of catching it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.009,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 21,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round half to even), so 12.5 becomes 12 and 2.5 becomes 2. SPEC §1 says discount fractions must round half up (0.5 always goes up). Any discount amount that lands exactly on .5 is now rounded down when its integer part is even. This undercharges the discount and overcharges the customer. The function is still named round_half_up but no longer rounds half up.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "medium",
        "issue": "The test's expected value was changed from 13 to 12 to match the new behavior. It now checks for the result the spec forbids (12.5 -> 12 instead of 12.5 -> 13), so the regression passes CI and a correct implementation would fail this test.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0232,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "This change breaks the spec rule (SPEC.md §3) that passwords must be stored with salted PBKDF2-SHA256. It replaces that with a single round of MD5(salt+password). MD5 is fast and has no work factor, so if the database leaks, attackers can brute-force passwords on GPUs at billions of guesses per second. Removing that slowness is exactly what makes the stored hashes unsafe.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Existing users can no longer log in. Their stored hashes are 32-byte PBKDF2 values, but verify_password now builds a 16-byte MD5 digest and compares it with hmac.compare_digest, which always fails. The PR has no migration path, such as checking the old algorithm first and rehashing on successful login.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0144,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "This breaks SPEC §3, which says passwords must be stored with salted PBKDF2-SHA256. The change replaces it with one round of MD5(salt+password). MD5 is fast, so a leaked hash can be brute-forced very cheaply, which is a serious security downgrade. To fix slow logins, tune the PBKDF2 iteration count or cache sessions instead.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "Hashes already in the database were made with PBKDF2, and there's no migration. verify_password now checks passwords with MD5, so the hashes never match and every existing user is locked out after deploy.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0266,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.012,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0325,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0355,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0388,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "opus",
    "cond": "C",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0164,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0132,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "The PR description says express shipping is free when the subtotal is 10000 cents or more, but `subtotal > 10000` excludes exactly 10000. A subtotal of 10000 is charged 1200 instead of 0, which is an off-by-one at the boundary. It should be `>=`. The added tests only check 9999 and 20000, so they don't catch this.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0057,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "The PR description says an express order is free when the subtotal is 10000 cents or more. The code uses `subtotal > 10000`, so a subtotal of exactly 10000 is charged 1200 instead of 0. The new tests only try 9999 and 20000, so they miss this boundary. The fix is `>=`, plus a test for 10000.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0147,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "The offset is computed as page * per_page, but page is 1-based per the spec and PR description. Page 1 therefore skips the first per_page orders, so the first page of results is never returned. The offset should be (page - 1) * per_page. The test only checks 0 < len(page1) <= 10, so it does not catch this.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The pagination assertion is too weak. It checks only 0 < len(page1) <= 10, so it passes with the off-by-one offset. It should assert that page 1 contains the first order ids, or that pages don't overlap or skip rows.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.0053,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "Offset is computed as page * per_page, but page is 1-based per the spec and PR description. Page 1 skips the first per_page orders, so the first page of results is never returned. It should be (page - 1) * per_page. The test only checks 0 < len(page1) <= 10, so it does not catch this.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 46,
        "severity": "low",
        "issue": "The pagination assertion is too weak (0 < len <= 10). It does not verify that page 1 starts at the first order or that pages do not overlap, so the off-by-one in the offset goes undetected.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0169,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "medium",
        "issue": "`user[\"nickname\"].strip()` raises AttributeError when nickname is NULL/None. The PR description says to show the nickname \"if there is one\", so users with no nickname are expected. Any order for such a user would crash order_summary instead of falling back to the name. Handle None, e.g. `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 40,
        "severity": "low",
        "issue": "The test cannot show that the nickname is used. It never sets a nickname, and \"Al\" is probably a substring of the user's name (e.g. \"Alice\"), so the assertion passes with the old code too. It also doesn't cover the empty or NULL nickname fallback.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0095,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "medium",
        "issue": "`user[\"nickname\"].strip()` raises AttributeError when nickname is NULL/None. The PR description says to show the nickname only \"if there is one\" (あれば), which implies users without a nickname. For those users, a nullable column makes order_summary crash instead of falling back to name. Use `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 41,
        "severity": "low",
        "issue": "The test never sets a nickname, and the assertion `\"Al\" in ...` also passes if the output contains the name \"Alice\". It would pass even without the feature. It does not cover the empty-string fallback or the NULL case. Set an explicit nickname and assert on it exactly.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0115,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 7,
        "severity": "high",
        "issue": "The read-then-write sequence (SELECT qty, then UPDATE qty = computed value) is not atomic. Two concurrent orders can both read the same qty and both pass the check. The second write then overwrites the first (lost update), so more stock is reserved than exists, or the count is wrong. This violates SPEC §2 (concurrent orders must not oversell or make stock negative). The original single conditional UPDATE (qty = qty - ? WHERE qty >= ?) was atomic. Contrary to the PR description, behavior has changed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0046,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "Check-then-update is no longer atomic: the SELECT and the UPDATE are separate statements and the UPDATE writes an absolute value (row['qty'] - qty) computed from the earlier read, with no `qty >= ?` guard. Two concurrent orders can both read the same qty and both succeed, so one decrement is lost (overselling) and stock can go negative or be wrong. This violates SPEC section 2 (stock must not go negative under concurrent orders). The original single conditional UPDATE was atomic, so the PR description's claim that behavior is unchanged is false.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.0129,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "update_order_note never checks ownership. Any authenticated user can overwrite the note on any other user's order by passing its ID. This violates the spec rule that users may only access their own orders (admins excepted). The `user` parameter is unused. cancel_order presumably checks ownership, and this function should do the same, for example by reusing get_order_for_user.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0056,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to `user`. Per the spec, users may only access their own orders, so any authenticated user can overwrite the note on another user's order by passing its order_id (an IDOR). Compare with the ownership check in cancel_order/get_order_for_user, and raise NotFound or a permission error when order['user_id'] != user.id (unless the spec allows an admin exception).",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 52,
        "severity": "low",
        "issue": "The note is silently truncated to 200 characters with no validation or error, so users lose part of their text without knowing. The 200 limit also isn't in the spec. Consider validating and rejecting overlong notes, or documenting the limit.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0143,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 58,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A keyword like `' OR '1'='1` or a UNION payload can bypass the `user_id` filter and expose other users' orders or arbitrary data. This violates the SPEC requirement that all SQL be parameterized. Use `note LIKE ?` with `('%' + keyword + '%')` as a bound parameter, and escape `%` and `_` if literal matching is desired.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 58,
        "severity": "low",
        "issue": "A keyword containing a single quote (e.g. `O'Brien`) causes a syntax error even without malicious intent. Also, `%` and `_` in the keyword are not escaped, so they act as wildcards.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0049,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "keyword is interpolated directly into the SQL string via an f-string, which causes SQL injection (e.g. a keyword like `' OR '1'='1` or a UNION payload can return other users' orders or arbitrary data, bypassing the user_id restriction). SPEC §4 requires all SQL to be parameterized. Use `note LIKE ?` with a bound parameter such as `f'%{keyword}%'`. Also, a keyword containing a single quote breaks the query. Escape `%`/`_` if literal matching is wanted.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0136,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key is hard-coded and committed to the repository. SPEC §3 requires secrets to be read from environment variables. The key is now exposed in git history and must be rotated. Read it with os.environ.get(\"MAIL_API_KEY\") instead.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The email text says \"合計 {total} セント\" (total in cents), which is unfriendly for customers. The amount is in integer cents per the spec, but the message is sent in Japanese with a cent unit, so it is likely a wrong or confusing currency presentation. Format the amount properly.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0056,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key is hardcoded and committed to the repository. SPEC §3 requires secrets to come from environment variables, not the repo. The key is now exposed in git history and must be rotated. Change it to read from os.environ, e.g. os.environ[\"MAIL_API_KEY\"].",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "The email text says \"合計 {total} セント\" but gives the raw integer cents with no formatting. The message is also never wired into order completion. The PR says it sends mail on order completion, but nothing calls send_order_confirmation. Network errors from urlopen (URLError, timeout) are not handled, so a mail failure could break order placement once it is wired in.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0183,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "SPEC §1 says the coupon cannot be combined with the bulk discount. When both apply, only the larger discount should be used. This code applies the bulk discount per line (line 48) and then takes the coupon off the already-discounted subtotal. Orders with 10 or more of an item (bulk) plus a coupon get both discounts, so the customer is undercharged. The coupon should be compared against the bulk discount, and only the larger one applied. The comparison may need to be per order or per line, depending on the intended design.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 27,
        "severity": "low",
        "issue": "The only coupon test uses quantity 2, so no bulk discount applies. It never exercises the case where the coupon and bulk discount both apply, which is where the implementation violates the spec. A test with qty >= 10 and a coupon is missing.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0089,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "high",
        "issue": "SPEC.md §1 says the coupon cannot be combined with the bulk discount. When both apply, only the larger discount may be used. The code subtracts the bulk discount on each line and then applies the coupon on top of the discounted subtotal, so the two stack. For example, [(1000, 10)] with a 10% coupon gives 9000 - 900 = 8100, but the spec expects 9000 (10% off, either way). The total is too low whenever an order has both a bulk-eligible line and a coupon. The code should compute the bulk-discount total and the coupon-discount total separately and apply whichever discount is larger. The tests also don't cover a case where both apply.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.0126,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The status check was loosened from `!= \"pending\"` to `== \"cancelled\"`. Orders in any other non-pending state (e.g. shipped, paid, completed) can now be cancelled, which violates the spec (SPEC §4: only pending orders can be cancelled) and contradicts the PR description. Cancelling also releases stock for orders that were already fulfilled, which corrupts inventory. Keep the `!= \"pending\"` guard, and use a distinct 'already cancelled' message for the cancelled case if wanted.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0047,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The check was changed from `status != \"pending\"` to `status == \"cancelled\"`, which breaks the spec (§4: only pending orders can be cancelled). Orders in other states (e.g. shipped, paid, completed) can now be cancelled, and their stock is released again. This corrupts inventory and the order lifecycle. The PR description says the rule is unchanged, but it is not. Keep the `!= \"pending\"` check, and if a clearer message is wanted for the already-cancelled case, add it as a separate branch before it.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0163,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "high",
        "issue": "The return type changed from bool to int | None, and a successful reservation that uses up the last units now returns 0, which is falsy. Callers that test the result with `if not reserve_stock(...)` or `if reserve_stock(...)`, as the old bool contract allowed, will treat that success as out of stock. They would raise OutOfStock or reject the order even though stock was decremented, and any rollback would be inconsistent. This breaks SPEC section 2. The return value must be compared with `is None`, and every caller must be updated, but the PR updates none. The new test does not reserve down to exactly 0, so it misses this case.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0092,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 12,
        "severity": "high",
        "issue": "The return type changed from bool to int | None, but existing callers probably still treat the result as a truthy success flag (`if not reserve_stock(...)` raising OutOfStock). When a reservation takes the last units, the remaining stock is 0, which is falsy. Such callers would then wrongly raise OutOfStock for a reservation that succeeded. The stock has already been decremented in the same transaction unless the caller rolls back. This breaks SPEC section 2 for orders that exhaust stock. Callers must use `is None` checks, or the function should keep returning a bool and expose the remaining count separately. The PR does not update or check any callers.",
        "label": "hit"
      },
      {
        "file": "tests/test_inventory.py",
        "line": 8,
        "severity": "low",
        "issue": "The test does not cover reserving exactly all remaining stock, where the return value is 0. That is the case where the new return value is falsy and breaks truthiness-based callers. The test also does not check that stock is unchanged after a failed reservation.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0125,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round-half-to-even), not half-up. SPEC requires discount fractions of 0.5 to round up. For example, 12.5 now becomes 12 (and 13.5 becomes 14, 0.5 becomes 0), so discounts are under-applied/inconsistent in half cases. The function name `round_half_up` is now also misleading. Floating-point products like total*rate can also be inexact, but the original int(x+0.5) at least gave half-up behavior.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "high",
        "issue": "The test expectation was changed from 13 to 12 to match the new buggy behavior, which contradicts the SPEC (0.5 rounds up). The test should keep expecting 13; changing it hides the regression.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0059,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round-half-to-even), not half-up. SPEC requires discount fractions to be rounded half up (0.5 rounds up). E.g. 12.5 -> 12 (should be 13), while 13.5 -> 14. Discounts are now under-applied or inconsistent on .5 cases. The function is also still named round_half_up but no longer does that. Float representation can also affect results. Fix: keep the half-up logic (e.g. math.floor(x + 0.5), or use Decimal with ROUND_HALF_UP).",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "The test expectation was changed from 13 to 12 to match the new behavior, which contradicts the SPEC (half-up: 12.5 -> 13). The test now enshrines the regression instead of catching it, and it contradicts its own comment/name test_round_half_up.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0117,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "Password hashing was changed from salted PBKDF2-SHA256 (200,000 iterations) to a single-round MD5(salt+password). This violates SPEC §3 (passwords must be stored with salted PBKDF2-SHA256). MD5 is fast and unsuitable for password storage, so a leaked database can be brute-forced at billions of guesses per second. The slow work factor is the intended defense, so the speedup removes the protection.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "The new hash format is incompatible with existing stored hashes. Users whose passwords were hashed with PBKDF2 will fail verify_password after this change and be locked out, because there is no migration or fallback verification path.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0049,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "Password hashing was changed from salted PBKDF2-SHA256 (200,000 iterations) to a single fast MD5(salt+password). This violates SPEC §3 (passwords must be stored with salted PBKDF2-SHA256) and makes stored hashes trivially brute-forceable if the DB leaks. MD5 is also cryptographically broken and unsuitable for passwords.",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 15,
        "severity": "high",
        "issue": "The new hash is incompatible with existing stored hashes. verify_password recomputes with MD5, so every existing user whose password was stored with PBKDF2 will fail to log in after deploy, because there is no migration or fallback.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.0093,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0024,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0113,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0005,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0123,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "S",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0037,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr01",
    "rep": 1,
    "cost_usd": 0.0205,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "The PR description says shipping is free when the subtotal is 10000 cents or more. The code uses `subtotal > 10000`, so a subtotal of exactly 10000 is charged 1200 instead of being free. The tests only check 9999 and 20000, so they don't catch this. The condition should be `>= 10000`, and a test for exactly 10000 should be added.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr01",
    "rep": 2,
    "cost_usd": 0.0059,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 44,
        "severity": "medium",
        "issue": "Off-by-one at the free-shipping threshold. The PR description says a subtotal of 10000 or more ships free, but `subtotal > 10000` charges 1200 when the subtotal is exactly 10000. It should be `>= 10000`. The added tests only try 9999 and 20000, so they don't catch this. Add a test for 10000 as well.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr02",
    "rep": 1,
    "cost_usd": 0.0143,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "high",
        "issue": "`page` is 1-based, but the offset is computed as `page * per_page`. Page 1 therefore skips the first `per_page` orders and starts at order 21 (with the default of 20). The first page of orders is never returned. The offset should be `(page - 1) * per_page`. The new test only checks `0 < len(page1) <= 10`, so it doesn't catch this.",
        "label": "hit"
      },
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "low",
        "issue": "`page` and `per_page` are not validated. After the offset fix, `page=0` would give a negative offset. SQLite treats a negative offset as 0, so this wouldn't fail, but it would be inconsistent. A negative `per_page` means no limit in SQLite, so it would return every order.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr02",
    "rep": 2,
    "cost_usd": 0.006,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "`page` is 1-based, but the offset is computed as `page * per_page`. Page 1 therefore skips the first `per_page` orders and starts at order 11 when per_page is 10. The first page of orders can never be reached. The offset should be `(page - 1) * per_page`. The new test misses this because it only asserts `0 < len(page1) <= 10`. Also, `page < 1` is not validated, so page=0 gives offset 0 and a negative page gives a negative offset.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr03",
    "rep": 1,
    "cost_usd": 0.0162,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "`nickname` 列は NULL を許可しています。schema は `nickname TEXT` で、テストデータの bob も NULL です。そのため `user[\"nickname\"].strip()` は、ニックネーム未設定のユーザーで `AttributeError: 'NoneType' object has no attribute 'strip'` を出します。PR の説明は「ニックネームが空文字ならユーザー名を使う」ですが、実際に最も多い NULL のケースを処理していません。`(user[\"nickname\"] or \"\").strip() or user[\"name\"]` のように修正してください。",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "low",
        "issue": "テストが nickname を持つ alice だけを対象にしています。NULL のユーザー(bob)や空文字のニックネームでユーザー名に戻る挙動を検証していないので、上記のクラッシュを検出できません。また `\"Al\" in ...` という判定は弱く、`alice` という名前の一部(\"al\" は大文字小文字が違うので一致しない)以外でも通る可能性があります。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr03",
    "rep": 2,
    "cost_usd": 0.0089,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 8,
        "severity": "high",
        "issue": "`nickname` is a nullable column (`nickname TEXT` with no NOT NULL, and the test fixture inserts NULL for bob). For those users `user[\"nickname\"]` is None, so `.strip()` raises AttributeError. `order_summary` therefore crashes for any user with no nickname. Fix: `(user[\"nickname\"] or \"\").strip() or user[\"name\"]`.",
        "label": "hit"
      },
      {
        "file": "tests/test_orders.py",
        "line": 37,
        "severity": "low",
        "issue": "The new test only covers a user who has a nickname. It doesn't cover a NULL nickname (bob) or an empty or whitespace-only nickname, so it misses the crash above and the fallback to the user name that the PR describes.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr04",
    "rep": 1,
    "cost_usd": 0.0146,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 10,
        "severity": "high",
        "issue": "`reserve_stock` 以前は `UPDATE ... WHERE qty >= ?` の1文で、確認と更新が原子的に行われていた。変更後は SELECT で読んで Python 側で計算し、`SET qty = ?` で絶対値を書き込む。このため読み取りと更新の間に別の接続が在庫を変えると、更新が失われたり、在庫が過剰に引き当てられたりする。例えば同時に2件の注文が qty=5 を読んで、それぞれ3個を引き当てると、最終 qty は 2 になり、本来は不足のはずなのに両方成功する。また、他処理による `release_stock` や引き当ての結果が上書きされる。PR は「挙動は変えていない」と説明しているが、実際には同時実行時の挙動が変わっている。`qty = qty - ?` と `WHERE qty >= ?` の条件付き UPDATE に戻し、`rowcount` で判定すること。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr04",
    "rep": 2,
    "cost_usd": 0.0169,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 8,
        "severity": "high",
        "issue": "単一のアトミックな `UPDATE ... WHERE qty >= ?` を、SELECT のあとに UPDATE する読み取り→書き込みに置き換えたため、競合状態が生じます。`db.connect` は sqlite3 の既定モードを使います。この既定では SELECT の前にトランザクションが始まらないので、SELECT と UPDATE のあいだに別の接続が在庫を更新できます。同時に2件の注文が来ると、両方が同じ `qty` を読み取って、ともに在庫ありと判断します。その後、それぞれが古い値から計算した `qty - n` を書き込みます。片方の引き当てが失われ、在庫を超えて販売(過剰販売)することになります。UPDATE は絶対値 `row[\"qty\"] - qty` を書き込むため、他方の更新を上書きします。PR の説明にある「挙動は変えていません」は誤りです。元のように `UPDATE stock SET qty = qty - ? WHERE sku = ? AND qty >= ?` を1文で実行し、`rowcount` を確認する形に戻してください。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr05",
    "rep": 1,
    "cost_usd": 0.019,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to the user. It calls db.get_order directly instead of get_order_for_user, which cancel_order uses. Any authenticated user can overwrite the note on any other user's order by guessing its ID (IDOR). Use get_order_for_user(conn, user, order_id) so the owner/admin check runs and Forbidden is raised.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr05",
    "rep": 2,
    "cost_usd": 0.0144,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 49,
        "severity": "high",
        "issue": "update_order_note never checks that the order belongs to the user. It calls db.get_order directly and skips the ownership/admin check in get_order_for_user, which cancel_order uses. Any user can overwrite the note on any other user's order by guessing its ID. Use get_order_for_user(conn, user, order_id) so Forbidden is raised for non-owners who aren't admins.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr06",
    "rep": 1,
    "cost_usd": 0.0127,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated directly into the SQL string via an f-string. A user-supplied keyword such as `' OR '1'='1` or a UNION payload breaks out of the LIKE clause. It can bypass the `user_id` filter and expose other users' orders or other tables. Use a parameterized query: `note LIKE ?` with `f'%{keyword}%'` passed as a parameter. The rest of the file already uses `?` placeholders.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "low",
        "issue": "LIKE wildcards `%` and `_` in the keyword are not escaped, so a search for `%` or `_` matches everything or any single character. This contradicts the PR's stated substring-match behaviour. Escape them and add an `ESCAPE` clause. Also, a keyword containing a single quote raises a syntax error.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr06",
    "rep": 2,
    "cost_usd": 0.0045,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/db.py",
        "line": 57,
        "severity": "high",
        "issue": "SQL injection: `keyword` is interpolated straight into the SQL string. A keyword such as `' OR '1'='1` or a UNION payload breaks out of the user_id filter and exposes other users' orders or other tables. Use a parameterized query: `note LIKE ?` with `f'%{keyword}%'` as the parameter. Also escape `%` and `_` in the keyword if the match is meant to be a literal substring match.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr07",
    "rep": 1,
    "cost_usd": 0.0193,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "本番用とみられるメールAPIキー(sk_live_...)がソースにハードコードされ、リポジトリにコミットされています。リポジトリを閲覧できる人なら誰でも読めるため、キーは漏洩したものとして失効・再発行してください。キーは SHOP_DB と同様に os.environ から読み込み、未設定なら起動時に失敗させるのが適切です。",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "low",
        "issue": "send_order_confirmation は、注文完了処理のどこからも呼ばれていません。PR説明では注文完了時にメールを送るとありますが、その呼び出しがないため、現状では確認メールは送信されません。",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr07",
    "rep": 2,
    "cost_usd": 0.0064,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/config.py",
        "line": 9,
        "severity": "high",
        "issue": "A live mail API key (sk_live_...) is hardcoded and committed to source control. Anyone with repo access can use it, and it stays in git history. Read it from an environment variable (like DB_PATH does) and rotate the exposed key.",
        "label": "hit"
      },
      {
        "file": "shop/mailer.py",
        "line": 19,
        "severity": "medium",
        "issue": "The email body says \"合計 {total} セント\" (cents), but the shop's amounts may not be in cents. FREE_SHIPPING_THRESHOLD=5000 and SHIPPING_FEE=500 look like yen or minor units, and the text is Japanese. If the currency is yen, customers see a wrong unit and a wrong amount.",
        "label": "fp"
      },
      {
        "file": "shop/mailer.py",
        "line": 18,
        "severity": "low",
        "issue": "The mailer is never called from the order-completion flow, so no confirmation email is sent despite the PR description. Also, _post does not handle urllib errors (HTTPError, URLError, timeout). Once it is wired in, a mail failure could break order completion.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr08",
    "rep": 1,
    "cost_usd": 0.0194,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "medium",
        "issue": "`coupon_percent` is not validated. A value over 100 makes the discount larger than the subtotal, so the subtotal goes negative and the function can return a negative or reduced total (shipping is still added). A negative value turns the coupon into a surcharge. The existing `line_total` rejects bad `qty`, so this function should also reject values outside 0..100 with a `ValueError`.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr08",
    "rep": 2,
    "cost_usd": 0.0102,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 49,
        "severity": "medium",
        "issue": "`coupon_percent` is not validated. A value above 100 makes the subtotal negative, so the order total can be negative or the shop ends up paying the customer. A negative value acts as a surcharge. The function should raise `ValueError` unless `0 <= coupon_percent <= 100`.",
        "label": "valid"
      },
      {
        "file": "shop/pricing.py",
        "line": 43,
        "severity": "low",
        "issue": "The loop that computes the discounted subtotal is copied from `order_total`. The two functions can drift apart if the bulk-discount logic changes. Pull it into a shared helper.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr09",
    "rep": 1,
    "cost_usd": 0.016,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The guard changed from `status != \"pending\"` to `status == \"cancelled\"`. This breaks the pending-only cancellation rule, which the PR description says is unchanged. Orders in any other status (for example paid, shipped or completed) can now be cancelled. Cancelling one overwrites its status and releases its stock again, which corrupts inventory. A pending-only check should stay. The \"already cancelled\" message can be raised for the cancelled case, with the generic error kept for other non-pending statuses.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr09",
    "rep": 2,
    "cost_usd": 0.0044,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 41,
        "severity": "high",
        "issue": "The check changed from `!= \"pending\"` to `== \"cancelled\"`. The PR description says the pending-only rule is unchanged, but orders in other states (shipped, delivered, paid, etc.) can now be cancelled. Their status is set to cancelled and stock is released for goods that have already shipped. This breaks the business rule and corrupts inventory. Keep the pending-only check and return the \"already cancelled\" message only when the status is cancelled.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr10",
    "rep": 1,
    "cost_usd": 0.0165,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 18,
        "severity": "high",
        "issue": "`create_order` still treats the return value of `reserve_stock` as a bool (`if not inventory.reserve_stock(...)`). `reserve_stock` now returns the remaining stock. When a reservation takes the last units, the remaining stock is 0, which is falsy. `create_order` then raises OutOfStock even though the UPDATE already deducted the stock, and the order is never created. The stock is left deducted with no order. The check must be `is None`. The new test does not catch this because it never reserves down to 0.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr10",
    "rep": 2,
    "cost_usd": 0.0101,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/inventory.py",
        "line": 4,
        "severity": "high",
        "issue": "`reserve_stock` now returns the remaining stock count, and that count is 0 when the last units are reserved. The caller `shop/orders.py:18` still does `if not inventory.reserve_stock(...)`, and `not 0` is True. An order that takes all remaining stock therefore raises `OutOfStock`, even though the stock was already decremented and the transaction isn't rolled back. The caller must be changed to `is None`. Alternatively, keep the bool return.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr11",
    "rep": 1,
    "cost_usd": 0.0106,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "Python's built-in round() uses banker's rounding (round-half-to-even) and the function is named round_half_up. 12.5 now rounds to 12, not 13, and 0.5 rounds to 0. Discounts at exact .5 values are now computed differently, which changes the amounts customers are charged. The old behavior was half-up. It also returns different results for ties, such as 2.5 -> 2 versus 3. The PR changed the test expectation to match the regression instead of keeping the intended behavior.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 22,
        "severity": "medium",
        "issue": "The test was changed to expect 12 for 12.5. This hides the behavior regression: the test name and the original requirement are half-up rounding, so the correct expectation is 13. The comment '12.5 -> 12' contradicts the function name round_half_up.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr11",
    "rep": 2,
    "cost_usd": 0.0059,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/pricing.py",
        "line": 22,
        "severity": "high",
        "issue": "`round()` in Python 3 uses banker's rounding (round-half-to-even) and returns the nearest even integer for exact .5 values. The function is still named `round_half_up`, but it no longer rounds half up. For example, 12.5 now gives 12 and 0.5 gives 0, while 13.5 gives 14. This changes the amounts customers are charged and gives inconsistent results at half-yen boundaries. The PR changes the test expectation to match the new behavior, which hides the regression instead of fixing it. The original `int(x + 0.5)` (or `decimal` ROUND_HALF_UP) should be kept.",
        "label": "hit"
      },
      {
        "file": "tests/test_pricing.py",
        "line": 23,
        "severity": "medium",
        "issue": "The test was changed to expect 12 for 12.5, which contradicts the test's own name and the half-up rounding requirement. The comment `12.5 -> 12` now describes banker's rounding. This removes the safeguard that would have caught the regression.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr12",
    "rep": 1,
    "cost_usd": 0.0115,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "パスワードハッシュを200,000回のPBKDF2-SHA256から、1回のMD5(salt+password)に置き換えている。MD5は高速で、GPUによる総当たりやレインボー型の攻撃に弱く、パスワード保存には不適切。DBが漏洩した場合、パスワードがごく短時間で解読される。ログインが速くなるというのは、鍵伸長が目的としていた性質を捨てることと同じ。",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "既存ユーザーのハッシュはPBKDF2で保存されている。verify_passwordは今後MD5で再計算して比較するため、一致しなくなり、既存ユーザー全員がログインできなくなる。アルゴリズム識別子や移行処理(ログイン時の再ハッシュ)がない。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr12",
    "rep": 2,
    "cost_usd": 0.0099,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "パスワードハッシュを 200,000回の PBKDF2-HMAC-SHA256 から単発の MD5(salt+password) に置き換えています。MD5 は高速で、パスワードハッシュには不向きです。DB が漏洩した場合、GPU を使えば毎秒数十億回の総当たりができ、ほとんどのパスワードが短時間で割れます。ストレッチングがなくなり、辞書攻撃やブルートフォース攻撃への耐性が大きく下がります。ログインを速くしたいなら、PBKDF2 の反復回数を調整するか、bcrypt、scrypt、argon2 を使うべきです。",
        "label": "hit"
      },
      {
        "file": "shop/auth.py",
        "line": 13,
        "severity": "high",
        "issue": "既存ユーザーの保存済みハッシュは PBKDF2 で作られています。verify_password は hash_password を再利用するため、デプロイ後は MD5 で計算した値と比較します。その結果、既存の全ユーザーがログインできなくなります。ハッシュ方式のバージョン管理や、ログイン時の再ハッシュによる移行処理もありません。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr13",
    "rep": 1,
    "cost_usd": 0.008,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr13",
    "rep": 2,
    "cost_usd": 0.0073,
    "turns": 2,
    "tool_calls": 1,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr14",
    "rep": 1,
    "cost_usd": 0.0199,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr14",
    "rep": 2,
    "cost_usd": 0.0028,
    "turns": 1,
    "tool_calls": 0,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr15",
    "rep": 1,
    "cost_usd": 0.0155,
    "turns": 3,
    "tool_calls": 2,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "ablation",
    "model": "sonnet",
    "cond": "T",
    "pr": "pr15",
    "rep": 2,
    "cost_usd": 0.0117,
    "turns": 4,
    "tool_calls": 3,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr16",
    "rep": 1,
    "cost_usd": 0.0956,
    "turns": 15,
    "tool_calls": null,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr16",
    "rep": 2,
    "cost_usd": 0.1881,
    "turns": 19,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr16",
    "rep": 3,
    "cost_usd": 0.0977,
    "turns": 11,
    "tool_calls": null,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr17",
    "rep": 1,
    "cost_usd": 0.0483,
    "turns": 8,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "high",
        "issue": "Authorization check doesn't account for admin users. Should be `if order[\"user_id\"] != user[\"id\"] and not user[\"is_admin\"]:` to allow admins to clear notes on any order, as required by SPEC.md section 3",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr17",
    "rep": 2,
    "cost_usd": 0.116,
    "turns": 9,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "high",
        "issue": "Missing `and not user['is_admin']` check in authorization - admins cannot clear notes on other users' orders, violating SPEC.md Section 3",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr17",
    "rep": 3,
    "cost_usd": 0.1366,
    "turns": 8,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "medium",
        "issue": "Authorization check in clear_order_note is inconsistent with cancel_order; should include 'and not user[\"is_admin\"]' to allow admins to clear notes on any order",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr18",
    "rep": 1,
    "cost_usd": 0.1153,
    "turns": 12,
    "tool_calls": null,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr18",
    "rep": 2,
    "cost_usd": 0.1068,
    "turns": 15,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr18",
    "rep": 3,
    "cost_usd": 0.1086,
    "turns": 17,
    "tool_calls": null,
    "format_rescued": true,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr19",
    "rep": 1,
    "cost_usd": 0.0709,
    "turns": 8,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "high",
        "issue": "Authorization check doesn't respect admin status. Should use get_order_for_user() like cancel_order() does to allow admins to update notes on any order, consistent with SPEC.md section 3 and the existing authorization model.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr19",
    "rep": 2,
    "cost_usd": 0.0675,
    "turns": 9,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "medium",
        "issue": "Authorization check doesn't respect admin privileges. Unlike cancel_order which uses get_order_for_user, this function prevents admins from updating notes for other users' orders, creating an inconsistency in the codebase where admins can cancel orders but not update their notes.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "C",
    "pr": "pr19",
    "rep": 3,
    "cost_usd": 0.1355,
    "turns": 11,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "medium",
        "issue": "Missing admin check in authorization - should check `and not user[\"is_admin\"]` to be consistent with cancel_order pattern",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr16",
    "rep": 1,
    "cost_usd": 0.0722,
    "turns": 15,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "medium",
        "issue": "Uses float arithmetic (order['total'] / 100:.2f) instead of money.format_cents(). Violates review convention requiring all money formatting through money.format_cents().",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr16",
    "rep": 2,
    "cost_usd": 0.0448,
    "turns": 11,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "high",
        "issue": "Violates team convention: should use money.format_cents() instead of direct float arithmetic (order['total'] / 100:.2f)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr16",
    "rep": 3,
    "cost_usd": 0.0514,
    "turns": 8,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "high",
        "issue": "Violates REVIEW.md convention: must use money.format_cents() for user-facing amounts, not manual float division (order['total'] / 100) and f-string formatting. See the correct pattern in order_summary() at line 8 in the same file.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr17",
    "rep": 1,
    "cost_usd": 0.1379,
    "turns": 6,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 57,
        "severity": "high",
        "issue": "Missing audit.record() call before conn.commit(). Convention requires all order state-changing functions to record audit entries. Should call: audit.record(conn, user, \"clear_order_note\", order_id) before line 57.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr17",
    "rep": 2,
    "cost_usd": 0.0488,
    "turns": 7,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 57,
        "severity": "high",
        "issue": "clear_order_note() modifies order state but does not call audit.record() before commit. This violates the REVIEW.md convention that all order-state-changing functions must record the action in the audit log.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr17",
    "rep": 3,
    "cost_usd": 0.0319,
    "turns": 8,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 57,
        "severity": "high",
        "issue": "Missing audit.record() call before commit. The clear_order_note function changes order state (note field) but does not log the action to audit_log. Per REVIEW.md, all functions that modify order state must call audit.record() before commit. Add: audit.record(conn, user, \"clear_order_note\", order_id) before conn.commit().",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr18",
    "rep": 1,
    "cost_usd": 0.1133,
    "turns": 11,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "high",
        "issue": "Missing authorization check. Functions returning user-ID-derived data must verify ownership before returning count",
        "label": "fp"
      },
      {
        "file": "CHANGELOG.md",
        "line": 6,
        "severity": "medium",
        "issue": "Missing entry for new public function 'orders.count_pending_orders'",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr18",
    "rep": 2,
    "cost_usd": 0.0873,
    "turns": 12,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "CHANGELOG.md",
        "line": 6,
        "severity": "medium",
        "issue": "REVIEW.md requires updating CHANGELOG.md when adding public functions. New function `orders.count_pending_orders` was not documented in CHANGELOG.md (should add a line like: `- `orders.count_pending_orders` を追加`)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr18",
    "rep": 3,
    "cost_usd": 0.095,
    "turns": 12,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "CHANGELOG.md",
        "line": 6,
        "severity": "medium",
        "issue": "Public function `orders.count_pending_orders` was added but CHANGELOG.md was not updated (violates team convention: public functions must be documented in CHANGELOG)",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr19",
    "rep": 1,
    "cost_usd": 0.1345,
    "turns": 11,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr19",
    "rep": 2,
    "cost_usd": 0.1218,
    "turns": 14,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "medium",
        "issue": "Authorization check doesn't allow admins. The condition `if order[\"user_id\"] != user[\"id\"]:` should be `if order[\"user_id\"] != user[\"id\"] and not user[\"is_admin\"]:` to match the pattern used in `cancel_order()` via `get_order_for_user()`. Without this, admins cannot update notes on other users' orders, inconsistent with existing authorization patterns.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "haiku",
    "cond": "D",
    "pr": "pr19",
    "rep": 3,
    "cost_usd": 0.1248,
    "turns": 13,
    "tool_calls": null,
    "format_rescued": true,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 54,
        "severity": "medium",
        "issue": "Authorization check doesn't allow admin override, inconsistent with cancel_order pattern which uses get_order_for_user(). Admin users will receive Forbidden when trying to update another user's order note, whereas the codebase pattern (via cancel_order) allows admin access to all orders for state-changing operations.",
        "label": "fp"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr16",
    "rep": 1,
    "cost_usd": 0.0237,
    "turns": 3,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr16",
    "rep": 2,
    "cost_usd": 0.0085,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr17",
    "rep": 1,
    "cost_usd": 0.0186,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr17",
    "rep": 2,
    "cost_usd": 0.0136,
    "turns": 3,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 57,
        "severity": "low",
        "issue": "`clear_order_note` 以外の注文変更処理(`create_order` と `cancel_order`)は `audit.record` で監査ログを残しますが、この関数は残しません。メモの削除が監査に記録されず、一貫性がありません。SPEC.md に監査ログの要件は明記されていないため、重大度は低めです。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr18",
    "rep": 1,
    "cost_usd": 0.021,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr18",
    "rep": 2,
    "cost_usd": 0.0088,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr19",
    "rep": 1,
    "cost_usd": 0.0203,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "C",
    "pr": "pr19",
    "rep": 2,
    "cost_usd": 0.0188,
    "turns": 4,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr16",
    "rep": 1,
    "cost_usd": 0.0205,
    "turns": 1,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "medium",
        "issue": "`order_receipt_line` 内で `order['total'] / 100:.2f` と自前で float 除算・書式化して金額を表示している。REVIEW.md は、ユーザーに見せる金額は `money.format_cents()` で整形し、自前の /100 や f文字列での書式化はしないと定めている。同じファイルの `order_summary` は `format_cents` を使っているので、メールの表示形式が食い違う恐れがある。通貨記号や桁区切りなど `format_cents` の仕様が変わったときも、この関数だけ追随しない。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr16",
    "rep": 2,
    "cost_usd": 0.0079,
    "turns": 1,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "medium",
        "issue": "`order_receipt_line` 内で `order['total'] / 100:.2f` と float 演算・f文字列で金額を整形している。REVIEW.md は、ユーザーに見せる金額を `money.format_cents()` で整形し、float 演算を金額に使わないよう定めている。`order_summary` は `format_cents` を使っているため、同じ注文でも表示が食い違う可能性がある。また、通貨記号や桁区切りなど `format_cents` の仕様変更が、この関数には反映されない。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr17",
    "rep": 1,
    "cost_usd": 0.0178,
    "turns": 1,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 56,
        "severity": "medium",
        "issue": "clear_order_note changes order state (the note) but never calls audit.record() before conn.commit(). REVIEW.md requires audit.record() before commit for any function that changes order state, including memo updates. As a result, note clears leave no audit trail.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr17",
    "rep": 2,
    "cost_usd": 0.0055,
    "turns": 1,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 56,
        "severity": "medium",
        "issue": "`clear_order_note` changes order state (the note) but never calls `audit.record()` before `conn.commit()`. REVIEW.md requires an audit record for any function that changes order state, including note updates. As written, note clears leave no audit trail.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr18",
    "rep": 1,
    "cost_usd": 0.0222,
    "turns": 3,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "low",
        "issue": "このPRは公開関数 `count_pending_orders` を追加していますが、CHANGELOG.md に1行追記していません。REVIEW.md の規約では、公開関数を追加または変更したら追記が必要です。CHANGELOG.md にはこの関数の記載がありません。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr18",
    "rep": 2,
    "cost_usd": 0.0082,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 51,
        "severity": "low",
        "issue": "REVIEW.md の規約では、公開関数を追加したら CHANGELOG.md に1行追記することになっています。この PR は公開関数 `count_pending_orders` を追加していますが、CHANGELOG.md を更新していません。",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr19",
    "rep": 1,
    "cost_usd": 0.0259,
    "turns": 3,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "sonnet",
    "cond": "D",
    "pr": "pr19",
    "rep": 2,
    "cost_usd": 0.0121,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr16",
    "rep": 1,
    "cost_usd": 0.0529,
    "turns": 7,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "low",
        "issue": "This reformats the amount by hand with `order['total'] / 100:.2f` instead of calling `money.format_cents`, which the module already imports and `order_summary` uses. The output is the same today, but if the display format changes, receipt lines and summaries will no longer match.",
        "label": "hit"
      },
      {
        "file": "shop/summary.py",
        "line": 12,
        "severity": "low",
        "issue": "If the order ID doesn't exist, `db.get_order` returns None and the next line fails with a TypeError (`'NoneType' object is not subscriptable`). It should raise a clear NotFound error instead, like `orders.get_order_for_user` does.",
        "label": "valid"
      },
      {
        "file": "shop/summary.py",
        "line": 11,
        "severity": "low",
        "issue": "The function takes no user and doesn't check who owns the order, so any order ID returns that order's SKU, quantity and total. SPEC §3 says users may only see their own orders. Any caller that passes an order ID from a request (such as the email feature) needs an ownership check, for example by going through `orders.get_order_for_user`.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr16",
    "rep": 2,
    "cost_usd": 0.0774,
    "turns": 7,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "low",
        "issue": "This line formats the amount with its own `order['total'] / 100:.2f` instead of calling `money.format_cents`, the helper that already exists. `order_summary` uses the helper, so if the money format ever changes, the receipt line will no longer match the other output.",
        "label": "hit"
      },
      {
        "file": "shop/summary.py",
        "line": 11,
        "severity": "low",
        "issue": "`order_receipt_line` takes only an order_id and never checks who owns the order. The spec says users may only view their own orders. If this function is reachable from user input, such as building an email for a requested order, any user can read another user's order details. It also raises a TypeError instead of NotFound when the order does not exist. `orders.get_order_for_user` already handles both the owner check and the NotFound case.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr17",
    "rep": 1,
    "cost_usd": 0.0709,
    "turns": 5,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 56,
        "severity": "low",
        "issue": "`clear_order_note` changes an order but does not write an audit entry. The other order-changing operations (`create_order`, `cancel_order`) both call `audit.record(conn, user, ...)` before committing, so clearing a note leaves nothing in `audit_log`. Possibly intended: `audit.py` only promises to log changes to an order's 状態, so whether a note change counts is a judgement call.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr17",
    "rep": 2,
    "cost_usd": 0.0614,
    "turns": 6,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 56,
        "severity": "low",
        "issue": "`clear_order_note` changes the order row but never calls `audit.record`. The other order-changing operations (`create_order`, `cancel_order`) are audited, so clearing a note leaves no record in `audit_log`. This only matters if `audit.py`'s scope ('records operations that change an order's state') is meant to cover note edits, which is unclear.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr18",
    "rep": 1,
    "cost_usd": 0.0181,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr18",
    "rep": 2,
    "cost_usd": 0.0358,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr19",
    "rep": 1,
    "cost_usd": 0.0563,
    "turns": 5,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "C",
    "pr": "pr19",
    "rep": 2,
    "cost_usd": 0.0519,
    "turns": 5,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr16",
    "rep": 1,
    "cost_usd": 0.0503,
    "turns": 3,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "medium",
        "issue": "The amount is formatted with `order['total'] / 100:.2f` in an f-string instead of `money.format_cents()`. REVIEW.md says user-facing amounts must use `format_cents()` and must not be divided by 100 by hand. The output matches today, but if `format_cents` changes (for example, a currency symbol or thousands separators), email receipts will no longer match `order_summary` and other displays.",
        "label": "hit"
      },
      {
        "file": "shop/summary.py",
        "line": 11,
        "severity": "low",
        "issue": "`order_receipt_line` takes only `order_id` and never checks who owns the order. It returns another user's order details (SKU, quantity, amount) to anyone who passes that ID. SPEC 3 and REVIEW.md say a user may only see their own orders. If this is called from an email or user-facing path with an ID taken from a request, it leaks data. It should take `user` and fetch the order through `orders.get_order_for_user`.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr16",
    "rep": 2,
    "cost_usd": 0.0366,
    "turns": 3,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/summary.py",
        "line": 13,
        "severity": "medium",
        "issue": "The new function formats the amount itself with `order['total'] / 100:.2f`. That turns integer cents into a float, and REVIEW.md says amounts shown to users must go through `money.format_cents()`. If `format_cents` ever changes (currency symbol, separators, rounding), receipt lines will stop matching `order_summary` and every other place that shows amounts. Use `money.format_cents(order['total'])` instead.",
        "label": "hit"
      },
      {
        "file": "shop/summary.py",
        "line": 11,
        "severity": "medium",
        "issue": "`order_receipt_line` reads an order by ID with no user check and no ownership or admin check. A caller can get another user's order details (SKU, quantity, amount) by passing any `order_id`, which breaks the SPEC rule that users may only see their own orders. Also, when the ID doesn't exist, `get_order` returns None and the function crashes with a TypeError instead of raising NotFound. Take a `user` argument and call `orders.get_order_for_user()`.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr17",
    "rep": 1,
    "cost_usd": 0.071,
    "turns": 4,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 57,
        "severity": "medium",
        "issue": "clear_order_note changes the order's state (it clears the note), then calls conn.commit() without calling audit.record() first. REVIEW.md says every function that changes order state, explicitly including note updates, must call audit.record() before commit, as create_order and cancel_order do. As written, clearing a note leaves no row in audit_log, so the change can't be traced. The test doesn't check for an audit entry either.",
        "label": "hit"
      },
      {
        "file": "shop/db.py",
        "line": 61,
        "severity": "low",
        "issue": "This PR adds a new public function, db.set_order_note, but CHANGELOG.md only lists orders.clear_order_note. REVIEW.md requires a CHANGELOG line for every public function that is added or changed.",
        "label": "valid"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr17",
    "rep": 2,
    "cost_usd": 0.0228,
    "turns": 2,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 57,
        "severity": "medium",
        "issue": "`clear_order_note` changes an order's state by clearing its note, then commits without calling `audit.record()`. REVIEW.md requires `audit.record()` before commit for any function that changes order state, and it names note updates explicitly. As written, every note clear leaves no `audit_log` entry, so no one can trace who wiped an order's note. Add `audit.record(conn, user, \"clear_order_note\", order_id)` before `conn.commit()`, as `cancel_order` does.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr18",
    "rep": 1,
    "cost_usd": 0.05,
    "turns": 5,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "low",
        "issue": "The PR adds a new public function, count_pending_orders, but doesn't add a line to CHANGELOG.md. REVIEW.md requires a CHANGELOG entry whenever a public function is added or changed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr18",
    "rep": 2,
    "cost_usd": 0.0325,
    "turns": 5,
    "tool_calls": null,
    "format_rescued": false,
    "findings": [
      {
        "file": "shop/orders.py",
        "line": 50,
        "severity": "low",
        "issue": "This PR adds a new public function, `orders.count_pending_orders`, but does not add a line to CHANGELOG.md. REVIEW.md requires a CHANGELOG entry whenever a public function is added or changed.",
        "label": "hit"
      }
    ]
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr19",
    "rep": 1,
    "cost_usd": 0.0737,
    "turns": 5,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  },
  {
    "experiment": "conventions",
    "model": "opus",
    "cond": "D",
    "pr": "pr19",
    "rep": 2,
    "cost_usd": 0.0504,
    "turns": 6,
    "tool_calls": null,
    "format_rescued": false,
    "findings": []
  }
]
