[
  {
    "id": "d3a28051-4876-4719-a90d-022d0626ae48",
    "sort_order": 0,
    "rubric_text": "In file `expense_reports.xlsx`, the file must exist in the workspace and contain exactly five worksheets named 'Amina Hassan', 'Leila Rahimi', 'Hiroshi Tanaka', 'Carlos Mendez', and 'Priya Natarajan'.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found in workspace'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open expense_reports.xlsx: {e}'}\n    required_sheets = {'Amina Hassan', 'Leila Rahimi', 'Hiroshi Tanaka', 'Carlos Mendez', 'Priya Natarajan'}\n    actual_sheets = set(wb.sheetnames)\n    missing = required_sheets - actual_sheets\n    if missing:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Missing worksheets: {missing}. Found: {actual_sheets}'}\n    return {'pass': True, 'score': 1.0, 'feedback': f'All 5 required worksheets found: {required_sheets}'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "685533ea-406a-4a1c-bb3f-d42dd95e87a3",
    "sort_order": 1,
    "rubric_text": "In file `expense_reports.xlsx`, each worksheet ('Amina Hassan', 'Leila Rahimi', 'Hiroshi Tanaka', 'Carlos Mendez', 'Priya Natarajan') must have row 1 as a header row containing exactly these eight columns in order: 'Jira Key', 'Date of Purchase', 'Merchant', 'Amount', 'Expense Category', 'Approved Amount', 'Denied Amount', 'Denial Reason'.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open file: {e}'}\n    required_headers = ['Jira Key', 'Date of Purchase', 'Merchant', 'Amount', 'Expense Category', 'Approved Amount', 'Denied Amount', 'Denial Reason']\n    sheets_to_check = ['Amina Hassan', 'Leila Rahimi', 'Hiroshi Tanaka', 'Carlos Mendez', 'Priya Natarajan']\n    failures = []\n    for sheet_name in sheets_to_check:\n        if sheet_name not in wb.sheetnames:\n            failures.append(f'Sheet \"{sheet_name}\" not found')\n            continue\n        ws = wb[sheet_name]\n        actual_headers = [str(ws.cell(row=1, column=i).value or '').strip() for i in range(1, 9)]\n        for i, (expected, actual) in enumerate(zip(required_headers, actual_headers), 1):\n            if expected.lower() != actual.lower():\n                failures.append(f'Sheet \"{sheet_name}\" col {i}: expected \"{expected}\", got \"{actual}\"')\n    if failures:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Header issues: ' + '; '.join(failures)}\n    return {'pass': True, 'score': 1.0, 'feedback': 'All 5 sheets have correct 8-column headers in row 1'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "f3da8b58-f81a-45a4-82eb-ade5d4f85521",
    "sort_order": 2,
    "rubric_text": "In file expense_reports.xlsx, the 'Amina Hassan' worksheet must contain all 17 required line-item rows with the correct Jira Keys, dates, merchants, amounts, expense categories, approved amounts, and denied amounts (0 or blank). Specifically: OPS-1 (4/23/2026, Shopify (promotional), 43.89, Client Relations Expense, approved 43.89), OPS-2 (5/5/2026, Uber, 63.18, Staff Expense, approved 63.18), OPS-3 (5/6/2026, Blackstone Chophouse, 46.86, Staff Expense, approved 46.86), OPS-4 (5/5/2026, Delta, 389.75, Staff Expense, approved 389.75), OPS-5 (5/6/2026, Helios Coffee Shop, 24.75, Staff Expense, approved 24.75), OPS-6 (5/6/2026, Gold Coast Eatery, 27.01, Staff Expense, approved 27.01), OPS-7 (5/6/2026, Asgard Restaurant, 541.33, Client Relations Expense, approved 541.33), OPS-8 (5/7/2026, Helios Coffee Shop, 24.75, Staff Expense, approved 24.75), OPS-9 (5/7/2026, Pilsen Table, 18.74, Staff Expense, approved 18.74), OPS-10 (5/7/2026, The Greektown Table, 45.75, Staff Expense, approved 45.75), OPS-11 (5/8/2026, Helios Coffee Shop, 24.48, Staff Expense, approved 24.48), OPS-12 (5/8/2026, Wicker Park Kitchen, 28.67, Staff Expense, approved 28.67), OPS-13 (5/7/2026, Melody Bistro, 747.49, Client Relations Expense, approved 747.49), OPS-14 (5/9/2026, Uber, 59.80, Staff Expense, approved 59.80), OPS-15 (5/9/2026, Helios Coffee Shop, 22.55, Staff Expense, approved 22.55), OPS-16 (5/9/2026, Bucktown Bites, 33.02, Staff Expense, approved 33.02), OPS-17 (5/5/2026 - 5/8/2026, Hyatt Regency Chicago, 2098.94, Staff Expense, approved 2098.94).",
    "verifier_code": "from pathlib import Path\nimport openpyxl\nimport re\nimport datetime\n\ndef normalize_str(s):\n    return str(s or '').strip().lower()\n\ndef parse_amount(val):\n    if val is None:\n        return None\n    s = str(val).replace('$', '').replace(',', '').strip()\n    try:\n        return float(s)\n    except:\n        return None\n\ndef parse_date_tuple(val):\n    \"\"\"Return (month, day, year) tuple, or None if unparseable.\"\"\"\n    if val is None:\n        return None\n    if isinstance(val, (datetime.datetime, datetime.date)):\n        return (val.month, val.day, val.year)\n    s = str(val).strip()\n    # M/D/YYYY or MM/DD/YYYY\n    m = re.match(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', s)\n    if m:\n        return (int(m.group(1)), int(m.group(2)), int(m.group(3)))\n    # YYYY-MM-DD\n    m = re.match(r'(\\d{4})-(\\d{1,2})-(\\d{1,2})', s)\n    if m:\n        return (int(m.group(2)), int(m.group(3)), int(m.group(1)))\n    return None\n\ndef date_matches(actual_cell, expected_str):\n    \"\"\"Flexible date comparison. Handles datetime objects, M/D/YYYY, YYYY-MM-DD,\n    and range strings like '5/5/2026 - 5/8/2026'.\"\"\"\n    exp_lower = expected_str.lower().strip()\n    # Range case: accept if actual text contains the range, or actual parses\n    # to either the start or end date of the range.\n    if '-' in exp_lower and '/' in exp_lower:\n        parts = re.findall(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', exp_lower)\n        if len(parts) >= 2:\n            start = (int(parts[0][0]), int(parts[0][1]), int(parts[0][2]))\n            end = (int(parts[1][0]), int(parts[1][1]), int(parts[1][2]))\n            act_str = normalize_str(actual_cell)\n            # Accept a textual range that mentions both dates\n            if all(f\"{m}/{d}\" in act_str or f\"{m:02d}/{d:02d}\" in act_str\n                   for m, d, _ in (start, end)):\n                return True\n            act_tuple = parse_date_tuple(actual_cell)\n            if act_tuple in (start, end):\n                return True\n            return False\n    # Single date case\n    exp_tuple = parse_date_tuple(expected_str)\n    act_tuple = parse_date_tuple(actual_cell)\n    if exp_tuple and act_tuple and exp_tuple == act_tuple:\n        return True\n    # Fallback: substring match on normalized strings\n    act_str = normalize_str(actual_cell)\n    if exp_lower in act_str or act_str in exp_lower:\n        return True\n    return False\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open: {e}'}\n    if 'Amina Hassan' not in wb.sheetnames:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Sheet \"Amina Hassan\" not found'}\n    ws = wb['Amina Hassan']\n    rows_by_key = {}\n    for row in ws.iter_rows(min_row=2, values_only=True):\n        if row[0] and str(row[0]).startswith('OPS-HR-TE-Hassan'):\n            rows_by_key[str(row[0]).strip()] = row\n    expected = [\n        ('OPS-1', '4/23/2026', 'shopify (promotional)', 43.89, 'client relations expense', 43.89),\n        ('OPS-2', '5/5/2026', 'uber', 63.18, 'staff expense', 63.18),\n        ('OPS-3', '5/6/2026', 'blackstone chophouse', 46.86, 'staff expense', 46.86),\n        ('OPS-4', '5/5/2026', 'delta', 389.75, 'staff expense', 389.75),\n        ('OPS-5', '5/6/2026', 'helios coffee shop', 24.75, 'staff expense', 24.75),\n        ('OPS-6', '5/6/2026', 'gold coast eatery', 27.01, 'staff expense', 27.01),\n        ('OPS-7', '5/6/2026', 'asgard restaurant', 541.33, 'client relations expense', 541.33),\n        ('OPS-8', '5/7/2026', 'helios coffee shop', 24.75, 'staff expense', 24.75),\n        ('OPS-9', '5/7/2026', 'pilsen table', 18.74, 'staff expense', 18.74),\n        ('OPS-10', '5/7/2026', 'the greektown table', 45.75, 'staff expense', 45.75),\n        ('OPS-11', '5/8/2026', 'helios coffee shop', 24.48, 'staff expense', 24.48),\n        ('OPS-12', '5/8/2026', 'wicker park kitchen', 28.67, 'staff expense', 28.67),\n        ('OPS-13', '5/7/2026', 'melody bistro', 747.49, 'client relations expense', 747.49),\n        ('OPS-14', '5/9/2026', 'uber', 59.80, 'staff expense', 59.80),\n        ('OPS-15', '5/9/2026', 'helios coffee shop', 22.55, 'staff expense', 22.55),\n        ('OPS-16', '5/9/2026', 'bucktown bites', 33.02, 'staff expense', 33.02),\n        ('OPS-17', '5/5/2026 - 5/8/2026', 'hyatt regency chicago', 2098.94, 'staff expense', 2098.94),\n    ]\n    failures = []\n    for jira_key, exp_date, exp_merchant, exp_amount, exp_category, exp_approved in expected:\n        if jira_key not in rows_by_key:\n            failures.append(f'Missing row for {jira_key}')\n            continue\n        row = rows_by_key[jira_key]\n        act_merchant = normalize_str(row[2])\n        act_amount = parse_amount(row[3])\n        act_category = normalize_str(row[4])\n        act_approved = parse_amount(row[5])\n        act_denied = parse_amount(row[6]) if row[6] is not None else 0.0\n        if not date_matches(row[1], exp_date):\n            failures.append(f'{jira_key}: date expected \"{exp_date}\", got \"{row[1]}\"')\n        if exp_merchant not in act_merchant and act_merchant not in exp_merchant:\n            failures.append(f'{jira_key}: merchant expected \"{exp_merchant}\", got \"{row[2]}\"')\n        if act_amount is None or abs(act_amount - exp_amount) > exp_amount * 0.01 + 0.01:\n            failures.append(f'{jira_key}: amount expected {exp_amount}, got {row[3]}')\n        if exp_category not in act_category and act_category not in exp_category:\n            failures.append(f'{jira_key}: category expected \"{exp_category}\", got \"{row[4]}\"')\n        if act_approved is None or abs(act_approved - exp_approved) > exp_approved * 0.01 + 0.01:\n            failures.append(f'{jira_key}: approved expected {exp_approved}, got {row[5]}')\n        if act_denied is None:\n            act_denied = 0.0\n        if abs(act_denied) > 0.05:\n            failures.append(f'{jira_key}: denied expected 0, got {row[6]}')\n    if failures:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Amina Hassan line item issues: ' + '; '.join(failures[:10])}\n    return {'pass': True, 'score': 1.0, 'feedback': f'All 17 Amina Hassan line items verified correctly'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "d5283083-687f-489b-9202-dd746e84afc2",
    "sort_order": 3,
    "rubric_text": "In file `expense_reports.xlsx`, the 'Amina Hassan' worksheet must contain a Report Summary section with: Jira Key = 'OPS-18', Total Approved = 4240.96 (±0.05), Total Denied = 0, Final Status = 'Approved', Manager Notified = 'N', and a Comment/Comments value of 'T&E review complete. All expenses approved.'",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef normalize_str(s):\n    return str(s or '').strip().lower()\n\ndef parse_amount(val):\n    if val is None:\n        return None\n    s = str(val).replace('$', '').replace(',', '').strip()\n    try:\n        return float(s)\n    except:\n        return None\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open: {e}'}\n    if 'Amina Hassan' not in wb.sheetnames:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Sheet \"Amina Hassan\" not found'}\n    ws = wb['Amina Hassan']\n    # Find the Report Summary section by scanning col A for 'report summary'\n    summary_start = None\n    for row in ws.iter_rows(min_row=1):\n        cell_a = normalize_str(row[0].value)\n        if 'report summary' in cell_a:\n            summary_start = row[0].row\n            break\n    if summary_start is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'No \"Report Summary\" section found in Amina Hassan sheet'}\n    # Read label->value pairs after summary_start\n    summary = {}\n    for r in range(summary_start + 1, summary_start + 20):\n        label = normalize_str(ws.cell(row=r, column=1).value)\n        value = ws.cell(row=r, column=2).value\n        if label:\n            summary[label] = value\n    failures = []\n    # Jira Key\n    jk = normalize_str(summary.get('jira key', ''))\n    if 'ops-hr-te-hassan-2026-05-11' not in jk:\n        failures.append(f'Jira Key expected OPS-18, got \"{summary.get(\"jira key\", \"\")}\"')\n    # Total Approved\n    ta = parse_amount(summary.get('total approved'))\n    if ta is None or abs(ta - 4240.96) > 0.05:\n        failures.append(f'Total Approved expected 4240.96 (±0.05), got {summary.get(\"total approved\")}')\n    # Total Denied\n    td = parse_amount(summary.get('total denied'))\n    if td is None:\n        td = 0.0\n    if abs(td) > 0.05:\n        failures.append(f'Total Denied expected 0, got {summary.get(\"total denied\")}')\n    # Final Status\n    fs = normalize_str(summary.get('final status', ''))\n    if 'approved' not in fs or 'partial' in fs or 'denied' in fs:\n        failures.append(f'Final Status expected \"Approved\", got \"{summary.get(\"final status\", \"\")}\"')\n    # Manager Notified\n    mn = normalize_str(summary.get('manager notified', ''))\n    if 'n' not in mn:\n        failures.append(f'Manager Notified expected \"N\", got \"{summary.get(\"manager notified\", \"\")}\"')\n    # Comments\n    comment_key = None\n    for k in summary:\n        if 'comment' in k:\n            comment_key = k\n            break\n    if comment_key is None:\n        failures.append('No Comment/Comments label found in summary')\n    else:\n        comment_val = normalize_str(summary[comment_key])\n        expected_comment = 't&e review complete. all expenses approved.'\n        if expected_comment not in comment_val and comment_val not in expected_comment:\n            failures.append(f'Comment expected \"T&E review complete. All expenses approved.\", got \"{summary[comment_key]}\"')\n    if failures:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Amina Hassan summary issues: ' + '; '.join(failures)}\n    return {'pass': True, 'score': 1.0, 'feedback': 'Amina Hassan Report Summary section verified: Jira Key OPS-18, Total Approved 4240.96, Total Denied 0, Final Status Approved, Manager Notified N, correct comment'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "81155ee6-2fb9-4155-a4a3-7b02b7eb9d34",
    "sort_order": 4,
    "rubric_text": "In file `expense_reports.xlsx`, the 'Leila Rahimi' worksheet must contain all 17 required line-item rows with correct Jira Keys, dates, merchants, amounts, expense categories, approved amounts, and denied amounts. Specifically all rows except OPS-50 must have Denied Amount = 0. Row OPS-50 (Lock & Lantern Escape Room, amount 308.61) must have Approved Amount = 192.88 (±1.50) and Denied Amount = 115.73 (±1.50).",
    "verifier_code": "from pathlib import Path\nimport openpyxl\nimport re\nimport datetime\n\ndef normalize_str(s):\n    return str(s or '').strip().lower()\n\ndef parse_amount(val):\n    if val is None:\n        return None\n    s = str(val).replace('$', '').replace(',', '').strip()\n    try:\n        return float(s)\n    except:\n        return None\n\ndef parse_date_tuple(val):\n    \"\"\"Return (month, day, year) tuple, or None if unparseable.\"\"\"\n    if val is None:\n        return None\n    if isinstance(val, (datetime.datetime, datetime.date)):\n        return (val.month, val.day, val.year)\n    s = str(val).strip()\n    m = re.match(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', s)\n    if m:\n        return (int(m.group(1)), int(m.group(2)), int(m.group(3)))\n    m = re.match(r'(\\d{4})-(\\d{1,2})-(\\d{1,2})', s)\n    if m:\n        return (int(m.group(2)), int(m.group(3)), int(m.group(1)))\n    return None\n\ndef date_matches(actual_cell, expected_str):\n    \"\"\"Flexible date comparison. Handles datetime objects, M/D/YYYY, YYYY-MM-DD,\n    and range strings like '5/5/2026 - 5/9/2026'.\"\"\"\n    exp_lower = expected_str.lower().strip()\n    if '-' in exp_lower and '/' in exp_lower:\n        parts = re.findall(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', exp_lower)\n        if len(parts) >= 2:\n            start = (int(parts[0][0]), int(parts[0][1]), int(parts[0][2]))\n            end = (int(parts[1][0]), int(parts[1][1]), int(parts[1][2]))\n            act_str = normalize_str(actual_cell)\n            if all(f\"{m}/{d}\" in act_str or f\"{m:02d}/{d:02d}\" in act_str\n                   for m, d, _ in (start, end)):\n                return True\n            act_tuple = parse_date_tuple(actual_cell)\n            if act_tuple in (start, end):\n                return True\n            return False\n    exp_tuple = parse_date_tuple(expected_str)\n    act_tuple = parse_date_tuple(actual_cell)\n    if exp_tuple and act_tuple and exp_tuple == act_tuple:\n        return True\n    act_str = normalize_str(actual_cell)\n    if exp_lower in act_str or act_str in exp_lower:\n        return True\n    return False\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open: {e}'}\n    if 'Leila Rahimi' not in wb.sheetnames:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Sheet \"Leila Rahimi\" not found'}\n    ws = wb['Leila Rahimi']\n    rows_by_key = {}\n    for row in ws.iter_rows(min_row=2, values_only=True):\n        if row[0] and str(row[0]).startswith('OPS-HR-TE-Rahimi'):\n            rows_by_key[str(row[0]).strip()] = row\n    expected = [\n        ('OPS-40', '4/21/2026', 'shopify (client gifts)', 119.54, 'client relations expense', 119.54, 0, 0.01, 0.05),\n        ('OPS-41', '5/5/2026', 'the greektown table', 47.96, 'staff expense', 47.96, 0, 0.01, 0.05),\n        ('OPS-42', '5/5/2026', 'delta', 384.40, 'staff expense', 384.40, 0, 0.01, 0.05),\n        ('OPS-43', '5/6/2026', 'helios coffee shop', 10.69, 'staff expense', 10.69, 0, 0.01, 0.05),\n        ('OPS-44', '5/6/2026', 'the riverwalk grille', 21.72, 'staff expense', 21.72, 0, 0.01, 0.05),\n        ('OPS-45', '5/7/2026', 'helios coffee shop', 12.40, 'staff expense', 12.40, 0, 0.01, 0.05),\n        ('OPS-46', '5/7/2026', 'gold coast eatery', 28.94, 'staff expense', 28.94, 0, 0.01, 0.05),\n        ('OPS-47', '5/7/2026', 'sable & oak kitchen', 37.76, 'staff expense', 37.76, 0, 0.01, 0.05),\n        ('OPS-48', '5/8/2026', 'helios coffee shop', 12.35, 'staff expense', 12.35, 0, 0.01, 0.05),\n        ('OPS-49', '5/8/2026', 'pilsen table', 24.81, 'staff expense', 24.81, 0, 0.01, 0.05),\n        ('OPS-50', '5/8/2026', 'lock & lantern escape room', 308.61, 'client relations expense', 192.88, 115.73, 1.50, 1.50),\n        ('OPS-51', '5/9/2026', 'helios coffee shop', 12.13, 'staff expense', 12.13, 0, 0.01, 0.05),\n        ('OPS-52', '5/9/2026', 'wicker park kitchen', 26.13, 'staff expense', 26.13, 0, 0.01, 0.05),\n        ('OPS-53', '5/9/2026', 'cosa nostra cucina', 49.06, 'staff expense', 49.06, 0, 0.01, 0.05),\n        ('OPS-54', '5/5/2026 - 5/9/2026', 'hyatt regency chicago', 1916.99, 'staff expense', 1916.99, 0, 0.01, 0.05),\n        ('OPS-55', '5/10/2026', 'uber', 57.00, 'staff expense', 57.00, 0, 0.01, 0.05),\n        ('OPS-56', '5/10/2026', 'helios coffee shop', 11.85, 'staff expense', 11.85, 0, 0.01, 0.05),\n    ]\n    failures = []\n    for jira_key, exp_date, exp_merchant, exp_amount, exp_category, exp_approved, exp_denied, tol_a, tol_d in expected:\n        if jira_key not in rows_by_key:\n            failures.append(f'Missing row for {jira_key}')\n            continue\n        row = rows_by_key[jira_key]\n        act_merchant = normalize_str(row[2])\n        act_amount = parse_amount(row[3])\n        act_category = normalize_str(row[4])\n        act_approved = parse_amount(row[5])\n        act_denied_raw = parse_amount(row[6])\n        act_denied = act_denied_raw if act_denied_raw is not None else 0.0\n        if not date_matches(row[1], exp_date):\n            failures.append(f'{jira_key}: date expected \"{exp_date}\", got \"{row[1]}\"')\n        if exp_merchant not in act_merchant and act_merchant not in exp_merchant:\n            failures.append(f'{jira_key}: merchant expected \"{exp_merchant}\", got \"{row[2]}\"')\n        if act_amount is None or abs(act_amount - exp_amount) > exp_amount * 0.01 + 0.01:\n            failures.append(f'{jira_key}: amount expected {exp_amount}, got {row[3]}')\n        if exp_category not in act_category and act_category not in exp_category:\n            failures.append(f'{jira_key}: category expected \"{exp_category}\", got \"{row[4]}\"')\n        if act_approved is None or abs(act_approved - exp_approved) > tol_a + 0.01:\n            failures.append(f'{jira_key}: approved expected {exp_approved} (±{tol_a}), got {row[5]}')\n        if abs(act_denied - exp_denied) > tol_d + 0.01:\n            failures.append(f'{jira_key}: denied expected {exp_denied} (±{tol_d}), got {row[6]}')\n    if failures:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Leila Rahimi line item issues: ' + '; '.join(failures[:10])}\n    return {'pass': True, 'score': 1.0, 'feedback': 'All 17 Leila Rahimi line items verified correctly including partial denial of Lock & Lantern Escape Room'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "10d0ebad-b9fe-4cd5-a939-7f8b7d912193",
    "sort_order": 5,
    "rubric_text": "In file expense_reports.xlsx, the 'Leila Rahimi' worksheet row for OPS-50 (Lock & Lantern Escape Room) must have a Denial Reason in column H that references more tickets purchased (8) than valid attendees (5, being 2 staff and 3 clients) and indicates 3 tickets were denied.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef normalize_str(s):\n    return str(s or '').strip().lower()\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open: {e}'}\n    if 'Leila Rahimi' not in wb.sheetnames:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Sheet \"Leila Rahimi\" not found'}\n    ws = wb['Leila Rahimi']\n    target_key = 'OPS-50'\n    denial_reason = None\n    for row in ws.iter_rows(min_row=2, values_only=True):\n        if row[0] and str(row[0]).strip() == target_key:\n            denial_reason = str(row[7] or '').strip() if len(row) > 7 else ''\n            break\n    if denial_reason is None:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Row {target_key} not found in Leila Rahimi sheet'}\n    if not denial_reason:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Row {target_key} has empty Denial Reason but should explain excess tickets'}\n    dr_lower = denial_reason.lower()\n    failures = []\n    # Must reference 8 tickets purchased\n    if '8' not in dr_lower:\n        failures.append('Denial reason does not mention 8 tickets purchased')\n    # Must reference 5 attendees (or 2 staff + 3 clients)\n    has_five = '5' in dr_lower\n    has_2staff_3clients = ('2' in dr_lower and '3' in dr_lower) or ('staff' in dr_lower and 'client' in dr_lower)\n    if not (has_five or has_2staff_3clients):\n        failures.append('Denial reason does not reference 5 valid attendees (2 staff + 3 clients)')\n    # Must reference 3 denied tickets\n    if '3' not in dr_lower:\n        failures.append('Denial reason does not mention 3 tickets denied')\n    if failures:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Lock & Lantern denial reason issues: {failures}. Actual reason: \"{denial_reason}\"'}\n    return {'pass': True, 'score': 1.0, 'feedback': f'Lock & Lantern Escape Room denial reason correctly references 8 tickets, 5 valid attendees, and 3 denied tickets. Reason: \"{denial_reason}\"'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "3f5acb9f-fb7d-482f-aa7d-90a4cae678bc",
    "sort_order": 6,
    "rubric_text": "In file `expense_reports.xlsx`, the 'Leila Rahimi' worksheet must contain a Report Summary section with: Jira Key = 'OPS-57', Total Approved = 2966.61 (±1.50), Total Denied = 115.73 (±1.50), Final Status = 'Partially Approved', Manager Notified = 'N', and Comment/Comments = 'T&E review complete. Expenses approved except where noted.'",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef normalize_str(s):\n    return str(s or '').strip().lower()\n\ndef parse_amount(val):\n    if val is None:\n        return None\n    s = str(val).replace('$', '').replace(',', '').strip()\n    try:\n        return float(s)\n    except:\n        return None\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'expense_reports.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'expense_reports.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open: {e}'}\n    if 'Leila Rahimi' not in wb.sheetnames:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Sheet \"Leila Rahimi\" not found'}\n    ws = wb['Leila Rahimi']\n    summary_start = None\n    for row in ws.iter_rows(min_row=1):\n        cell_a = normalize_str(row[0].value)\n        if 'report summary' in cell_a:\n            summary_start = row[0].row\n            break\n    if summary_start is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'No \"Report Summary\" section found in Leila Rahimi sheet'}\n    summary = {}\n    for r in range(summary_start + 1, summary_start + 20):\n        label = normalize_str(ws.cell(row=r, column=1).value)\n        value = ws.cell(row=r, column=2).value\n        if label:\n            summary[label] = value\n    failures = []\n    jk = normalize_str(summary.get('jira key', ''))\n    if 'ops-hr-te-rahimi-2026-05-11' not in jk:\n        failures.append(f'Jira Key expected OPS-57, got \"{summary.get(\"jira key\", \"\")}\"')\n    ta = parse_amount(summary.get('total approved'))\n    if ta is None or abs(ta - 2966.61) > 1.50:\n        failures.append(f'Total Approved expected 2966.61 (±1.50), got {summary.get(\"total approved\")}')\n    td = parse_amount(summary.get('total denied'))\n    if td is None:\n        td = 0.0\n    if abs(td - 115.73) > 1.50:\n        failures.append(f'Total Denied expected 115.73 (±1.50), got {summary.get(\"total denied\")}')\n    fs = normalize_str(summary.get('final status', ''))\n    if 'partial' not in fs:\n        failures.append(f'Final Status expected \"Partially Approved\", got \"{summary.get(\"final status\", \"\")}\"')\n    mn = normalize_str(summary.get('manager notified', ''))\n    if 'n' not in mn:\n        failures.append(f'Manager Notified expected \"N\", got \"{summary.get(\"manager notified\", \"\")}\"')\n    comment_key = None\n    for k in summary:\n        if 'comment' in k:\n            comment_key = k\n            break\n    if comment_key is None:\n        failures.append('No Comment/Comments label found in summary')\n    else:\n        comment_val = normalize_str(summary[comment_key])\n        expected_comment = 't&e review complete. expenses approved except where noted.'\n        if expected_comment not in comment_val and comment_val not in expected_comment:\n            failures.append(f'Comment expected \"T&E review complete. Expenses approved except where noted.\", got \"{summary[comment_key]}\"')\n    if failures:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Leila Rahimi summary issues: ' + '; '.join(failures)}\n    return {'pass': True, 'score': 1.0, 'feedback': 'Leila Rahimi Report Summary verified: Jira Key OPS-57, Total Approved 2966.61, Total Denied 115.73, Final Status Partially Approved, Manager Notified N, correct comment'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "8db7af74-e1a4-4f57-b4f7-7186ebe398d9",
    "sort_order": 7,
    "rubric_text": "6. In the worksheet \"Hiroshi Tanaka\", each of the following line-item rows must exist (identified by the Jira Key in column A) with the listed values in columns B through H. Row order is not graded. A Denial Reason is not required for any row and all Denial Reason cells must be empty. A Denied Amount of 0 and an empty cell are both acceptable. Dollar amounts match on numeric value and ignore currency symbols or thousands separators. For mileage rows, the Amount column (D) may appear as the mileage string such as \"357 miles\" or as the computed dollar amount; the Approved Amount column (F) must be the dollar reimbursement at the IRS rate of 0.725 per mile.\nRow OPS-59: Date 5/5/2026, Merchant \"Self drove (mileage)\", Amount \"357 miles\" or equivalent, Expense Category \"Staff Expense\", Approved Amount 258.83 (tolerance plus or minus 0.05), Denied Amount 0.\nRow OPS-58: Date 5/5/2026, Merchant \"Sable & Oak Kitchen\", Amount 38.59, Expense Category \"Staff Expense\", Approved Amount 38.59, Denied Amount 0.\nRow OPS-60: Date 5/6/2026, Merchant \"Gloriana Bakery\", Amount 9.10, Expense Category \"Staff Expense\", Approved Amount 9.10, Denied Amount 0.\nRow OPS-61: Date 5/6/2026, Merchant \"Wicker Park Kitchen\", Amount 21.22, Expense Category \"Staff Expense\", Approved Amount 21.22, Denied Amount 0.\nRow OPS-62: Date 5/6/2026, Merchant \"Harbor Light Bistro\", Amount 45.20, Expense Category \"Staff Expense\", Approved Amount 45.20, Denied Amount 0.\nRow OPS-63: Date 5/7/2026, Merchant \"Gloriana Bakery\", Amount 10.47, Expense Category \"Staff Expense\", Approved Amount 10.47, Denied Amount 0.\nRow OPS-64: Date 5/7/2026, Merchant \"Bucktown Bites\", Amount 26.40, Expense Category \"Staff Expense\", Approved Amount 26.40, Denied Amount 0.\nRow OPS-65: Date 5/7/2026, Merchant \"Mariscos Del Lago\", Amount 33.07, Expense Category \"Staff Expense\", Approved Amount 33.07, Denied Amount 0.\nRow OPS-66: Date 5/8/2026, Merchant \"Gloriana Bakery\", Amount 11.02, Expense Category \"Staff Expense\", Approved Amount 11.02, Denied Amount 0.\nRow OPS-67: Date 5/8/2026, Merchant \"Lincoln Square Cafe\", Amount 26.13, Expense Category \"Staff Expense\", Approved Amount 26.13, Denied Amount 0.\nRow OPS-68: Date 5/9/2026, Merchant \"Gloriana Bakery\", Amount 11.85, Expense Category \"Staff Expense\", Approved Amount 11.85, Denied Amount 0.\nRow OPS-69: Date 5/9/2026, Merchant \"Loop Street Deli\", Amount 25.63, Expense Category \"Staff Expense\", Approved Amount 25.63, Denied Amount 0.\nRow OPS-70: Date 5/9/2026, Merchant \"Koto Ramen & Izakaya\", Amount 39.69, Expense Category \"Staff Expense\", Approved Amount 39.69, Denied Amount 0.\nRow OPS-71: Date \"5/5/2026 - 5/9/2026\", Merchant \"Hyatt Regency Chicago\", Amount 1916.99, Expense Category \"Staff Expense\", Approved Amount 1916.99, Denied Amount 0.\nRow OPS-72: Date 5/10/2026, Merchant \"Gloriana Bakery\", Amount 11.30, Expense Category \"Staff Expense\", Approved Amount 11.30, Denied Amount 0.\nRow OPS-73: Date 5/10/2026, Merchant \"Self drove (mileage)\", Amount \"360 miles\" or equivalent, Expense Category \"Staff Expense\", Approved Amount 261.00 (tolerance plus or minus 0.05), Denied Amount 0.",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\nfrom datetime import datetime\n\ndef verify(workspace_path, external_services_path=None):\n    \"\"\"\n    Verify rubric item 6: Hiroshi Tanaka worksheet line items in expense_reports.xlsx\n    \"\"\"\n    feedback_parts = []\n    total_checks = 0\n    passed_checks = 0\n\n    # Find the xlsx file\n    ws = Path(workspace_path)\n    xlsx_files = list(ws.glob('**/expense_reports.xlsx'))\n    if not xlsx_files:\n        xlsx_files = list(ws.glob('**/*.xlsx'))\n    if not xlsx_files:\n        return {'pass': False, 'score': 0.0, 'feedback': 'No xlsx file found in workspace'}\n\n    xlsx_path = xlsx_files[0]\n\n    try:\n        import openpyxl\n        wb = openpyxl.load_workbook(str(xlsx_path), data_only=True)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open {xlsx_path.name}: {e}'}\n\n    # Find the Hiroshi Tanaka worksheet\n    target_sheet = None\n    for name in wb.sheetnames:\n        if 'tanaka' in name.lower() and 'hiroshi' in name.lower():\n            target_sheet = wb[name]\n            break\n    if target_sheet is None:\n        for name in wb.sheetnames:\n            if 'tanaka' in name.lower():\n                target_sheet = wb[name]\n                break\n    if target_sheet is None:\n        return {'pass': False, 'score': 0.0, 'feedback': f'No worksheet found for Hiroshi Tanaka. Sheets: {wb.sheetnames}'}\n\n    feedback_parts.append(f'Found worksheet: \"{target_sheet.title}\"')\n\n    # Read all rows into a dict keyed by Jira Key (column A)\n    rows_by_key = {}\n    for row in target_sheet.iter_rows(min_row=1, max_col=8, values_only=False):\n        cell_a = row[0].value\n        if cell_a is None:\n            continue\n        key_str = str(cell_a).strip()\n        if key_str.startswith('OPS-HR-TE-Tanaka'):\n            row_vals = [c.value for c in row]\n            rows_by_key[key_str] = row_vals\n\n    def parse_amount(val):\n        \"\"\"Parse a dollar amount from various formats, return float or None.\"\"\"\n        if val is None:\n            return None\n        if isinstance(val, (int, float)):\n            return float(val)\n        s = str(val).strip()\n        # Remove $, commas\n        s = s.replace('$', '').replace(',', '').strip()\n        try:\n            return float(s)\n        except ValueError:\n            return None\n\n    def parse_date(val):\n        \"\"\"Normalize date to a comparable string. Return lowercased string.\"\"\"\n        if val is None:\n            return ''\n        if isinstance(val, datetime):\n            return val.strftime('%-m/%-d/%Y').replace('/0', '/').lstrip('0') if False else val.strftime('%m/%d/%Y').lstrip('0').replace('/0', '/')\n        s = str(val).strip()\n        return s\n\n    def dates_match(actual, expected):\n        \"\"\"Loosely compare dates.\"\"\"\n        if actual is None:\n            return False\n        # Normalize both\n        def norm(d):\n            if isinstance(d, datetime):\n                return d.strftime('%Y-%m-%d')\n            s = str(d).strip()\n            # Try multiple parsing\n            for fmt in ['%m/%d/%Y', '%Y-%m-%d', '%m/%d/%y', '%d/%m/%Y']:\n                try:\n                    return datetime.strptime(s, fmt).strftime('%Y-%m-%d')\n                except ValueError:\n                    continue\n            return s.lower()\n        # For date ranges like \"5/5/2026 - 5/9/2026\"\n        if '-' in str(expected) and '/' in str(expected):\n            # It's a range; just check containment loosely\n            act_str = str(actual).replace(' ', '').lower()\n            exp_str = str(expected).replace(' ', '').lower()\n            # Normalize slashes\n            def norm_date_str(s):\n                # Remove leading zeros\n                return re.sub(r'\\b0+(\\d)', r'\\1', s)\n            return norm_date_str(act_str) == norm_date_str(exp_str) or \\\n                   norm_date_str(exp_str) in norm_date_str(act_str) or \\\n                   norm_date_str(act_str) in norm_date_str(exp_str) or \\\n                   (norm(str(expected).split('-')[0].strip()) in norm(actual) if isinstance(actual, str) else False)\n        return norm(actual) == norm(expected)\n\n    def merchant_matches(actual, expected):\n        if actual is None:\n            return False\n        a = str(actual).strip().lower()\n        e = expected.strip().lower()\n        # Fuzzy: check containment or near match\n        if a == e:\n            return True\n        if e in a or a in e:\n            return True\n        # Remove punctuation\n        def depunct(s):\n            return re.sub(r'[^a-z0-9 ]', '', s)\n        return depunct(a) == depunct(e)\n\n    def category_matches(actual, expected):\n        if actual is None:\n            return False\n        a = str(actual).strip().lower()\n        e = expected.strip().lower()\n        return a == e or e in a or a in e\n\n    def amount_close(actual, expected, tol=0.05):\n        a = parse_amount(actual)\n        if a is None:\n            return False\n        return abs(a - expected) <= tol\n\n    def denied_is_zero(val):\n        if val is None or str(val).strip() == '':\n            return True\n        a = parse_amount(val)\n        if a is None:\n            return True  # empty/non-numeric is fine\n        return abs(a) < 0.01\n\n    def denial_reason_empty(val):\n        if val is None:\n            return True\n        s = str(val).strip()\n        return s == '' or s.lower() == 'none' or s == '0'\n\n    def is_mileage_amount(val, expected_miles):\n        \"\"\"Check if the amount cell is either the mileage string or the dollar equivalent.\"\"\"\n        if val is None:\n            return False\n        s = str(val).strip().lower()\n        # Check for mileage string\n        if str(expected_miles) in s and 'mile' in s:\n            return True\n        # Check for dollar equivalent\n        dollar = expected_miles * 0.725\n        a = parse_amount(val)\n        if a is not None and abs(a - dollar) < 0.10:\n            return True\n        if a is not None and abs(a - expected_miles) < 0.10:\n            return True\n        return False\n\n    # Define expected rows\n    # Format: (jira_key, date, merchant, amount_or_miles, is_mileage, miles_count, category, approved_amount, approved_tol, denied_zero)\n    expected_rows = [\n        ('OPS-59', '5/5/2026', 'Self drove (mileage)', None, True, 357, 'Staff Expense', 258.83, 0.05),\n        ('OPS-58', '5/5/2026', 'Sable & Oak Kitchen', 38.59, False, 0, 'Staff Expense', 38.59, 0.05),\n        ('OPS-60', '5/6/2026', 'Gloriana Bakery', 9.10, False, 0, 'Staff Expense', 9.10, 0.05),\n        ('OPS-61', '5/6/2026', 'Wicker Park Kitchen', 21.22, False, 0, 'Staff Expense', 21.22, 0.05),\n        ('OPS-62', '5/6/2026', 'Harbor Light Bistro', 45.20, False, 0, 'Staff Expense', 45.20, 0.05),\n        ('OPS-63', '5/7/2026', 'Gloriana Bakery', 10.47, False, 0, 'Staff Expense', 10.47, 0.05),\n        ('OPS-64', '5/7/2026', 'Bucktown Bites', 26.40, False, 0, 'Staff Expense', 26.40, 0.05),\n        ('OPS-65', '5/7/2026', 'Mariscos Del Lago', 33.07, False, 0, 'Staff Expense', 33.07, 0.05),\n        ('OPS-66', '5/8/2026', 'Gloriana Bakery', 11.02, False, 0, 'Staff Expense', 11.02, 0.05),\n        ('OPS-67', '5/8/2026', 'Lincoln Square Cafe', 26.13, False, 0, 'Staff Expense', 26.13, 0.05),\n        ('OPS-68', '5/9/2026', 'Gloriana Bakery', 11.85, False, 0, 'Staff Expense', 11.85, 0.05),\n        ('OPS-69', '5/9/2026', 'Loop Street Deli', 25.63, False, 0, 'Staff Expense', 25.63, 0.05),\n        ('OPS-70', '5/9/2026', 'Koto Ramen & Izakaya', 39.69, False, 0, 'Staff Expense', 39.69, 0.05),\n        ('OPS-71', '5/5/2026 - 5/9/2026', 'Hyatt Regency Chicago', 1916.99, False, 0, 'Staff Expense', 1916.99, 0.05),\n        ('OPS-72', '5/10/2026', 'Gloriana Bakery', 11.30, False, 0, 'Staff Expense', 11.30, 0.05),\n        ('OPS-73', '5/10/2026', 'Self drove (mileage)', None, True, 360, 'Staff Expense', 261.00, 0.05),\n    ]\n\n    for exp in expected_rows:\n        jira_key, exp_date, exp_merchant, exp_amount, is_mileage, miles, exp_category, exp_approved, tol = exp\n        total_checks += 1\n\n        if jira_key not in rows_by_key:\n            feedback_parts.append(f'FAIL: Row {jira_key} not found in worksheet')\n            continue\n\n        row = rows_by_key[jira_key]\n        # Columns: A=JiraKey(0), B=Date(1), C=Merchant(2), D=Amount(3), E=Category(4), F=Approved(5), G=Denied(6), H=DenialReason(7)\n        row_ok = True\n        row_issues = []\n\n        # Check Date (col B, index 1)\n        if not dates_match(row[1], exp_date):\n            row_ok = False\n            row_issues.append(f'Date: expected \"{exp_date}\", got \"{row[1]}\"')\n\n        # Check Merchant (col C, index 2)\n        if not merchant_matches(row[2], exp_merchant):\n            row_ok = False\n            row_issues.append(f'Merchant: expected \"{exp_merchant}\", got \"{row[2]}\"')\n\n        # Check Amount (col D, index 3)\n        if is_mileage:\n            if not is_mileage_amount(row[3], miles):\n                row_ok = False\n                row_issues.append(f'Amount: expected \"{miles} miles\" or ${miles*0.725:.2f}, got \"{row[3]}\"')\n        else:\n            if not amount_close(row[3], exp_amount, 0.05):\n                row_ok = False\n                row_issues.append(f'Amount: expected {exp_amount}, got \"{row[3]}\"')\n\n        # Check Category (col E, index 4)\n        if not category_matches(row[4], exp_category):\n            row_ok = False\n            row_issues.append(f'Category: expected \"{exp_category}\", got \"{row[4]}\"')\n\n        # Check Approved Amount (col F, index 5)\n        if not amount_close(row[5], exp_approved, tol):\n            row_ok = False\n            row_issues.append(f'Approved Amount: expected {exp_approved} (±{tol}), got \"{row[5]}\"')\n\n        # Check Denied Amount (col G, index 6)\n        denied_val = row[6] if len(row) > 6 else None\n        if not denied_is_zero(denied_val):\n            row_ok = False\n            row_issues.append(f'Denied Amount: expected 0 or empty, got \"{denied_val}\"')\n\n        # Check Denial Reason (col H, index 7) must be empty\n        reason_val = row[7] if len(row) > 7 else None\n        if not denial_reason_empty(reason_val):\n            row_ok = False\n            row_issues.append(f'Denial Reason: expected empty, got \"{reason_val}\"')\n\n        if row_ok:\n            passed_checks += 1\n            feedback_parts.append(f'PASS: Row {jira_key} matches expected values')\n        else:\n            feedback_parts.append(f'FAIL: Row {jira_key}: {\"; \".join(row_issues)}')\n\n    score = passed_checks / total_checks if total_checks > 0 else 0.0\n    passed = score >= 0.8  # Allow some tolerance - pass if 80%+ rows correct\n\n    feedback = f'Hiroshi Tanaka worksheet: {passed_checks}/{total_checks} rows correct (score={score:.2f}).\\n' + '\\n'.join(feedback_parts)\n    return {'pass': passed, 'score': score, 'feedback': feedback}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776176680649",
    "sort_order": 8,
    "rubric_text": "The \"Hiroshi Tanaka\" worksheet in `expense_reports.xlsx` must contain a Report Summary section after the last line item with the following labels in column A and the listed values in column B (row order within the section is not graded). Monetary values may appear as numbers or as the value returned by a SUM formula over the Approved Amount or Denied Amount column; currency symbols and thousands separators are ignored.\n\n- Label \"Jira Key\" with value \"OPS-74\".\n- Label \"Total Approved\" with value 2746.49 (tolerance ±1.00).\n- Label \"Total Denied\" with value 0.\n- Label \"Final Status\" with value \"Approved\".\n- Label \"Manager Notified\" with value \"N\".\n- Label \"Comment\" (or \"Comments\") with value \"T&E review complete. All expenses approved.\"",
    "verifier_code": "from pathlib import Path\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n    try:\n        import openpyxl\n    except ImportError:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"openpyxl not available\"}\n\n    file_path = Path(workspace_path) / \"expense_reports.xlsx\"\n    if not file_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"File not found: {file_path}\"}\n\n    wb = openpyxl.load_workbook(str(file_path), data_only=True)\n\n    # Find Hiroshi Tanaka worksheet (case-insensitive match)\n    target_ws = None\n    for name in wb.sheetnames:\n        if name.strip().lower() == \"hiroshi tanaka\":\n            target_ws = wb[name]\n            break\n\n    if target_ws is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"Worksheet 'Hiroshi Tanaka' not found. Available sheets: {wb.sheetnames}\"}\n\n    # Collect all rows as (col_a, col_b) pairs\n    # We need to find the Report Summary section and then look for label-value pairs\n    rows = []\n    for row in target_ws.iter_rows(min_row=1, max_col=2, values_only=True):\n        rows.append(row)\n\n    # Find the Report Summary header row\n    summary_start = None\n    for i, (a, b) in enumerate(rows):\n        if a is not None and str(a).strip().lower().replace(\" \", \"\") == \"reportsummary\":\n            summary_start = i\n            break\n\n    if summary_start is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"Could not find 'Report Summary' header in column A of Hiroshi Tanaka worksheet.\"}\n\n    # Extract label->value mapping from summary section\n    summary = {}\n    for i in range(summary_start + 1, len(rows)):\n        a, b = rows[i]\n        if a is not None:\n            key = str(a).strip()\n            summary[key.lower()] = (key, b)\n\n    feedback_parts = []\n    checks_passed = 0\n    total_checks = 6\n\n    def clean_num(val):\n        \"\"\"Try to extract a numeric value from val.\"\"\"\n        if val is None:\n            return None\n        if isinstance(val, (int, float)):\n            return float(val)\n        s = str(val).strip()\n        # Remove currency symbols, commas, spaces\n        s = re.sub(r'[\\$€£¥,\\s]', '', s)\n        try:\n            return float(s)\n        except ValueError:\n            return None\n\n    def find_label(possible_keys):\n        \"\"\"Find a summary entry matching any of the possible key strings (case-insensitive).\"\"\"\n        for pk in possible_keys:\n            pk_lower = pk.lower()\n            for sk in summary:\n                if pk_lower in sk or sk in pk_lower:\n                    return summary[sk]\n        return None\n\n    # 1. Jira Key\n    entry = find_label([\"jira key\", \"jirakey\", \"jira\"])\n    if entry:\n        orig_label, val = entry\n        val_str = str(val).strip() if val is not None else \"\"\n        if val_str.upper() == \"OPS-HR-TE-TANAKA-2026-05-11\" or val_str == \"OPS-74\":\n            checks_passed += 1\n            feedback_parts.append(f\"PASS: Jira Key = '{val_str}'\")\n        else:\n            feedback_parts.append(f\"FAIL: Jira Key expected 'OPS-74', got '{val_str}'\")\n    else:\n        feedback_parts.append(\"FAIL: No 'Jira Key' label found in Report Summary.\")\n\n    # 2. Total Approved ~ 2746.49 ± 1.00\n    entry = find_label([\"total approved\", \"totalapproved\"])\n    if entry:\n        orig_label, val = entry\n        num = clean_num(val)\n        if num is not None and abs(num - 2746.49) <= 1.00:\n            checks_passed += 1\n            feedback_parts.append(f\"PASS: Total Approved = {num}\")\n        else:\n            feedback_parts.append(f\"FAIL: Total Approved expected ~2746.49 (±1.00), got '{val}' (parsed as {num})\")\n    else:\n        feedback_parts.append(\"FAIL: No 'Total Approved' label found in Report Summary.\")\n\n    # 3. Total Denied = 0\n    entry = find_label([\"total denied\", \"totaldenied\"])\n    if entry:\n        orig_label, val = entry\n        num = clean_num(val)\n        if num is not None and abs(num - 0) <= 0.01:\n            checks_passed += 1\n            feedback_parts.append(f\"PASS: Total Denied = {num}\")\n        else:\n            # Also accept string \"0\" or blank/None as 0\n            val_str = str(val).strip() if val is not None else \"\"\n            if val_str in [\"\", \"0\", \"0.0\", \"0.00\", \"$0\", \"$0.00\"]:\n                checks_passed += 1\n                feedback_parts.append(f\"PASS: Total Denied = '{val_str}' (treated as 0)\")\n            else:\n                feedback_parts.append(f\"FAIL: Total Denied expected 0, got '{val}' (parsed as {num})\")\n    else:\n        feedback_parts.append(\"FAIL: No 'Total Denied' label found in Report Summary.\")\n\n    # 4. Final Status = Approved\n    entry = find_label([\"final status\", \"finalstatus\", \"status\"])\n    if entry:\n        orig_label, val = entry\n        val_str = str(val).strip() if val is not None else \"\"\n        if val_str.lower() == \"approved\":\n            checks_passed += 1\n            feedback_parts.append(f\"PASS: Final Status = '{val_str}'\")\n        else:\n            feedback_parts.append(f\"FAIL: Final Status expected 'Approved', got '{val_str}'\")\n    else:\n        feedback_parts.append(\"FAIL: No 'Final Status' label found in Report Summary.\")\n\n    # 5. Manager Notified = N\n    entry = find_label([\"manager notified\", \"managernotified\"])\n    if entry:\n        orig_label, val = entry\n        val_str = str(val).strip() if val is not None else \"\"\n        if val_str.upper() in [\"N\", \"NO\"]:\n            checks_passed += 1\n            feedback_parts.append(f\"PASS: Manager Notified = '{val_str}'\")\n        else:\n            feedback_parts.append(f\"FAIL: Manager Notified expected 'N', got '{val_str}'\")\n    else:\n        feedback_parts.append(\"FAIL: No 'Manager Notified' label found in Report Summary.\")\n\n    # 6. Comment(s) = \"T&E review complete. All expenses approved.\"\n    entry = find_label([\"comment\", \"comments\"])\n    if entry:\n        orig_label, val = entry\n        val_str = str(val).strip() if val is not None else \"\"\n        expected = \"T&E review complete. All expenses approved.\"\n        # Flexible comparison: case-insensitive, ignore trailing/leading whitespace\n        if val_str.lower() == expected.lower() or expected.lower() in val_str.lower():\n            checks_passed += 1\n            feedback_parts.append(f\"PASS: Comments = '{val_str}'\")\n        else:\n            feedback_parts.append(f\"FAIL: Comments expected '{expected}', got '{val_str}'\")\n    else:\n        feedback_parts.append(\"FAIL: No 'Comment'/'Comments' label found in Report Summary.\")\n\n    score = checks_passed / total_checks\n    passed = checks_passed == total_checks\n    feedback = f\"Hiroshi Tanaka Report Summary: {checks_passed}/{total_checks} checks passed.\\n\" + \"\\n\".join(feedback_parts)\n\n    return {\"pass\": passed, \"score\": score, \"feedback\": feedback}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776176742950",
    "sort_order": 9,
    "rubric_text": "In the worksheet \"Carlos Mendez\" of `expense_reports.xlsx`, each of the following line-item rows must exist (identified by the Jira Key in column A) with the listed values in columns B through H. Row order is not graded. A Denial Reason is required only where noted; for all other rows the Denial Reason cell must be empty. A Denied Amount of 0 and an empty cell are both acceptable. Dollar amounts match on numeric value and ignore currency symbols or thousands separators.\n\n1. **OPS-19**: Date 5/5/2026, Merchant \"Delta\", Amount 384.40, Expense Category \"Staff Expense\", Approved Amount 384.40, Denied Amount 0.\n2. **OPS-20**: Date 5/5/2026, Merchant \"Cosa Nostra Cucina\", Amount 46.86, Expense Category \"Staff Expense\", Approved Amount 46.86, Denied Amount 0.\n3. **OPS-21**: Date 5/6/2026, Merchant \"Bucktown Bites\", Amount 28.11, Expense Category \"Staff Expense\", Approved Amount 28.11, Denied Amount 0.\n4. **OPS-22**: Date 5/6/2026, Merchant \"Sable & Oak Kitchen\", Amount 48.23, Expense Category \"Staff Expense\", Approved Amount 48.23, Denied Amount 0.\n5. **OPS-23**: Date 5/6/2026, Merchant \"Art Institute of Chicago\", Amount 32.00, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 32.00. Denial Reason must state or be equivalent to \"Missing business justification. Please submit personal reimbursement.\"\n6. **OPS-24**: Date 5/7/2026, Merchant \"Lincoln Square Cafe\", Amount 24.25, Expense Category \"Staff Expense\", Approved Amount 24.25, Denied Amount 0.\n7. **OPS-25**: Date 5/7/2026, Merchant \"Blackstone Chophouse\", Amount 46.86, Expense Category \"Staff Expense\", Approved Amount 46.86, Denied Amount 0.\n8. **OPS-26**: Date 5/8/2026, Merchant \"Loop Street Deli\", Amount 21.22, Expense Category \"Staff Expense\", Approved Amount 21.22, Denied Amount 0.\n9. **OPS-27**: Date 5/8/2026, Merchant \"Cosa Nostra Cucina\", Amount 43.55, Expense Category \"Staff Expense\", Approved Amount 43.55, Denied Amount 0.\n10. **OPS-28**: Date 5/9/2026, Merchant \"Gloriana Bakery\", Amount 13.73, Expense Category \"Staff Expense\", Approved Amount 13.73, Denied Amount 0.\n11. **OPS-29**: Date 5/9/2026, Merchant \"The Riverwalk Grille\", Amount 36.93, Expense Category \"Staff Expense\", Approved Amount 36.93, Denied Amount 0.\n12. **OPS-30**: Date 5/9/2026, Merchant \"Harbor Light Bistro\", Amount 43.55, Expense Category \"Staff Expense\", Approved Amount 43.55, Denied Amount 0.\n13. **OPS-31**: Date \"5/5/2026 - 5/9/2026\", Merchant \"Hyatt Regency Chicago\", Amount 1916.99, Expense Category \"Staff Expense\", Approved Amount 1916.99, Denied Amount 0.",
    "verifier_code": "from pathlib import Path\nimport re\nimport datetime\n\ndef verify(workspace_path, external_services_path=None):\n    import openpyxl\n\n    filepath = Path(workspace_path) / \"expense_reports.xlsx\"\n    if not filepath.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"File expense_reports.xlsx not found.\"}\n\n    wb = openpyxl.load_workbook(str(filepath))\n\n    sheet = None\n    for name in wb.sheetnames:\n        if \"carlos\" in name.lower() and \"mendez\" in name.lower():\n            sheet = wb[name]\n            break\n    if sheet is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"Worksheet 'Carlos Mendez' not found. Available sheets: {wb.sheetnames}\"}\n\n    rows_by_key = {}\n    for row in sheet.iter_rows(min_row=1, values_only=False):\n        cell_a = row[0].value\n        if cell_a and isinstance(cell_a, str) and \"Mendez\" in cell_a and \"OPS\" in cell_a:\n            key = cell_a.strip()\n            vals = [c.value for c in row]\n            rows_by_key[key] = vals\n\n    def parse_num(v):\n        if v is None:\n            return 0.0\n        if isinstance(v, (int, float)):\n            return float(v)\n        s = str(v).strip().replace(\"$\", \"\").replace(\",\", \"\").strip()\n        if s == \"\" or s.lower() == \"none\":\n            return 0.0\n        try:\n            return float(s)\n        except ValueError:\n            return None\n\n    def nums_close(a, b, tol=0.05):\n        if a is None or b is None:\n            return False\n        if abs(b) < 0.01:\n            return abs(a) < 0.01 + tol\n        return abs(a - b) <= max(abs(b) * tol, 0.05)\n\n    def normalize_str(s):\n        if s is None:\n            return \"\"\n        return str(s).strip().lower()\n\n    def parse_date_tuple(val):\n        if val is None:\n            return None\n        if isinstance(val, (datetime.datetime, datetime.date)):\n            return (val.month, val.day, val.year)\n        s = str(val).strip()\n        m = re.match(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', s)\n        if m:\n            return (int(m.group(1)), int(m.group(2)), int(m.group(3)))\n        m = re.match(r'(\\d{4})-(\\d{1,2})-(\\d{1,2})', s)\n        if m:\n            return (int(m.group(2)), int(m.group(3)), int(m.group(1)))\n        return None\n\n    def date_matches(actual_cell, expected_str):\n        exp_lower = expected_str.lower().strip()\n        # Range expected\n        if ' - ' in exp_lower:\n            parts = re.findall(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', exp_lower)\n            if len(parts) >= 2:\n                start = (int(parts[0][0]), int(parts[0][1]), int(parts[0][2]))\n                end = (int(parts[1][0]), int(parts[1][1]), int(parts[1][2]))\n                act_str = normalize_str(actual_cell)\n                if all(f\"{m}/{d}\" in act_str or f\"{m:02d}/{d:02d}\" in act_str\n                       for m, d, _ in (start, end)):\n                    return True\n                act_tuple = parse_date_tuple(actual_cell)\n                if act_tuple in (start, end):\n                    return True\n                return False\n        # Single date expected\n        exp_tuple = parse_date_tuple(expected_str)\n        act_tuple = parse_date_tuple(actual_cell)\n        if exp_tuple and act_tuple and exp_tuple == act_tuple:\n            return True\n        act_str = normalize_str(actual_cell)\n        if exp_lower in act_str or act_str in exp_lower:\n            return True\n        return False\n\n    def merchant_matches(cell_val, expected):\n        if cell_val is None:\n            return False\n        return normalize_str(cell_val) == normalize_str(expected) or normalize_str(expected) in normalize_str(cell_val)\n\n    def category_matches(cell_val, expected):\n        return normalize_str(cell_val) == normalize_str(expected)\n\n    def denial_reason_blank(cell_val):\n        v = normalize_str(cell_val)\n        return v == \"\" or v == \"none\" or v == \"n/a\" or v == \"0\"\n\n    def denial_reason_present(cell_val, keywords):\n        v = normalize_str(cell_val)\n        if v == \"\" or v == \"none\":\n            return False\n        for kw in keywords:\n            if kw.lower() not in v:\n                return False\n        return True\n\n    expected = [\n        {\"key\": \"OPS-19\", \"date\": \"5/5/2026\", \"merchant\": \"Delta\", \"amount\": 384.40, \"category\": \"Staff Expense\", \"approved\": 384.40, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-20\", \"date\": \"5/5/2026\", \"merchant\": \"Cosa Nostra Cucina\", \"amount\": 46.86, \"category\": \"Staff Expense\", \"approved\": 46.86, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-21\", \"date\": \"5/6/2026\", \"merchant\": \"Bucktown Bites\", \"amount\": 28.11, \"category\": \"Staff Expense\", \"approved\": 28.11, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-22\", \"date\": \"5/6/2026\", \"merchant\": \"Sable & Oak Kitchen\", \"amount\": 48.23, \"category\": \"Staff Expense\", \"approved\": 48.23, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-23\", \"date\": \"5/6/2026\", \"merchant\": \"Art Institute of Chicago\", \"amount\": 32.00, \"category\": \"Staff Expense\", \"approved\": 0, \"denied\": 32.00, \"denial_reason_required\": True, \"denial_keywords\": [\"business justification\", \"personal\"]},\n        {\"key\": \"OPS-24\", \"date\": \"5/7/2026\", \"merchant\": \"Lincoln Square Cafe\", \"amount\": 24.25, \"category\": \"Staff Expense\", \"approved\": 24.25, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-25\", \"date\": \"5/7/2026\", \"merchant\": \"Blackstone Chophouse\", \"amount\": 46.86, \"category\": \"Staff Expense\", \"approved\": 46.86, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-26\", \"date\": \"5/8/2026\", \"merchant\": \"Loop Street Deli\", \"amount\": 21.22, \"category\": \"Staff Expense\", \"approved\": 21.22, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-27\", \"date\": \"5/8/2026\", \"merchant\": \"Cosa Nostra Cucina\", \"amount\": 43.55, \"category\": \"Staff Expense\", \"approved\": 43.55, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-28\", \"date\": \"5/9/2026\", \"merchant\": \"Gloriana Bakery\", \"amount\": 13.73, \"category\": \"Staff Expense\", \"approved\": 13.73, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-29\", \"date\": \"5/9/2026\", \"merchant\": \"The Riverwalk Grille\", \"amount\": 36.93, \"category\": \"Staff Expense\", \"approved\": 36.93, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-30\", \"date\": \"5/9/2026\", \"merchant\": \"Harbor Light Bistro\", \"amount\": 43.55, \"category\": \"Staff Expense\", \"approved\": 43.55, \"denied\": 0, \"denial_reason_required\": False},\n        {\"key\": \"OPS-31\", \"date\": \"5/5/2026 - 5/9/2026\", \"merchant\": \"Hyatt Regency Chicago\", \"amount\": 1916.99, \"category\": \"Staff Expense\", \"approved\": 1916.99, \"denied\": 0, \"denial_reason_required\": False},\n    ]\n\n    errors = []\n    passed = 0\n    total = len(expected)\n\n    for exp in expected:\n        key = exp[\"key\"]\n        found_row = None\n        for rk, rv in rows_by_key.items():\n            if normalize_str(rk) == normalize_str(key):\n                found_row = rv\n                break\n        if found_row is None:\n            for rk, rv in rows_by_key.items():\n                if normalize_str(key) in normalize_str(rk) or normalize_str(rk) in normalize_str(key):\n                    found_row = rv\n                    break\n        if found_row is None:\n            errors.append(f\"Row {key}: Not found in worksheet.\")\n            continue\n\n        row_errors = []\n        if not date_matches(found_row[1], exp[\"date\"]):\n            row_errors.append(f\"Date: expected '{exp['date']}', got '{found_row[1]}'\")\n        if not merchant_matches(found_row[2], exp[\"merchant\"]):\n            row_errors.append(f\"Merchant: expected '{exp['merchant']}', got '{found_row[2]}'\")\n        actual_amount = parse_num(found_row[3])\n        if not nums_close(actual_amount, exp[\"amount\"]):\n            row_errors.append(f\"Amount: expected {exp['amount']}, got {found_row[3]}\")\n        if not category_matches(found_row[4], exp[\"category\"]):\n            row_errors.append(f\"Category: expected '{exp['category']}', got '{found_row[4]}'\")\n        actual_approved = parse_num(found_row[5])\n        if not nums_close(actual_approved, exp[\"approved\"]):\n            row_errors.append(f\"Approved: expected {exp['approved']}, got {found_row[5]}\")\n        actual_denied = parse_num(found_row[6])\n        if not nums_close(actual_denied, exp[\"denied\"]):\n            row_errors.append(f\"Denied: expected {exp['denied']}, got {found_row[6]}\")\n        if exp[\"denial_reason_required\"]:\n            if not denial_reason_present(found_row[7] if len(found_row) > 7 else None, exp.get(\"denial_keywords\", [])):\n                row_errors.append(f\"Denial Reason: expected reason containing {exp.get('denial_keywords', [])}, got '{found_row[7] if len(found_row) > 7 else None}'\")\n        else:\n            if len(found_row) > 7 and not denial_reason_blank(found_row[7]):\n                row_errors.append(f\"Denial Reason: expected blank, got '{found_row[7]}'\")\n\n        if row_errors:\n            errors.append(f\"Row {key}: \" + \"; \".join(row_errors))\n        else:\n            passed += 1\n\n    score = passed / total if total > 0 else 0.0\n    if errors:\n        feedback = f\"Passed {passed}/{total} rows.\\nErrors:\\n\" + \"\\n\".join(errors)\n    else:\n        feedback = f\"All {total} rows verified successfully for Carlos Mendez.\"\n\n    return {\"pass\": passed == total, \"score\": score, \"feedback\": feedback}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776176745296",
    "sort_order": 10,
    "rubric_text": "The \"Carlos Mendez\" worksheet in `expense_reports.xlsx` must contain a Report Summary section after the last line item with the following labels in column A and the listed values in column B (row order within the section is not graded). Monetary values may appear as numbers or as the value returned by a SUM formula over the Approved Amount or Denied Amount column; currency symbols and thousands separators are ignored.\n\n- Label \"Jira Key\" with value `OPS-32`.\n- Label \"Total Approved\" with value `2654.68` (tolerance ±1.00).\n- Label \"Total Denied\" with value `32.00`.\n- Label \"Final Status\" with value `Partially Approved`.\n- Label \"Manager Notified\" with value `N`.\n- Label \"Comment\" (or \"Comments\") with value `T&E review complete. Expenses approved except where noted.`",
    "verifier_code": "from pathlib import Path\nimport re\nimport openpyxl\n\n\ndef verify(workspace_path, external_services_path=None):\n    file_path = Path(workspace_path) / \"expense_reports.xlsx\"\n    if not file_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"File not found: {file_path}\"}\n\n    wb = openpyxl.load_workbook(str(file_path), data_only=True)\n\n    # Find the Carlos Mendez worksheet (case-insensitive, flexible spacing)\n    target_ws = None\n    for name in wb.sheetnames:\n        if re.search(r'carlos\\s+mendez', name, re.IGNORECASE):\n            target_ws = wb[name]\n            break\n\n    if target_ws is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"No worksheet matching 'Carlos Mendez' found. Sheets: {wb.sheetnames}\"}\n\n    # Read all rows into a list of (colA, colB) tuples\n    rows = []\n    for row in target_ws.iter_rows(min_row=1, max_col=2, values_only=True):\n        a_val = row[0] if row[0] is not None else \"\"\n        b_val = row[1] if len(row) > 1 and row[1] is not None else \"\"\n        rows.append((a_val, b_val))\n\n    # Find the Report Summary section\n    summary_start = None\n    for i, (a, b) in enumerate(rows):\n        if isinstance(a, str) and re.search(r'report\\s+summary', a, re.IGNORECASE):\n            summary_start = i\n            break\n\n    if summary_start is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"'Report Summary' header not found in column A.\"}\n\n    # Build a dict of label -> value from the summary section\n    summary = {}\n    for a, b in rows[summary_start + 1:]:\n        if isinstance(a, str) and a.strip():\n            summary[a.strip().lower()] = b\n\n    # Also check starting from summary_start itself in case labels include that row\n    a0, b0 = rows[summary_start]\n    if isinstance(a0, str) and a0.strip().lower() != 'report summary':\n        summary[a0.strip().lower()] = b0\n\n    def clean_num(val):\n        \"\"\"Extract a numeric value from a cell, stripping currency symbols, commas, etc.\"\"\"\n        if val is None:\n            return None\n        if isinstance(val, (int, float)):\n            return float(val)\n        s = str(val).replace('$', '').replace(',', '').replace(' ', '').strip()\n        try:\n            return float(s)\n        except ValueError:\n            return None\n\n    def find_key(dictionary, *candidates):\n        \"\"\"Find a value in dict by trying multiple key variations (case-insensitive).\"\"\"\n        for key in dictionary:\n            for c in candidates:\n                if re.fullmatch(c, key, re.IGNORECASE):\n                    return dictionary[key]\n        return None\n\n    checks_passed = 0\n    total_checks = 6\n    feedback_parts = []\n\n    # 1. Jira Key\n    jira_val = find_key(summary, r'jira\\s*key')\n    if jira_val is not None and re.search(r'OPS-32', str(jira_val), re.IGNORECASE):\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Jira Key: found '{jira_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Jira Key: expected 'OPS-32', found '{jira_val}'\")\n\n    # 2. Total Approved ~2654.68 ± 1.00\n    approved_val = find_key(summary, r'total\\s*approved')\n    approved_num = clean_num(approved_val)\n    if approved_num is not None and abs(approved_num - 2654.68) <= 1.00:\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Total Approved: {approved_num}\")\n    else:\n        feedback_parts.append(f\"FAIL Total Approved: expected ~2654.68 (±1.00), found '{approved_val}' (numeric: {approved_num})\")\n\n    # 3. Total Denied == 32.00 (use ±1.00 tolerance)\n    denied_val = find_key(summary, r'total\\s*denied')\n    denied_num = clean_num(denied_val)\n    if denied_num is not None and abs(denied_num - 32.00) <= 1.00:\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Total Denied: {denied_num}\")\n    else:\n        feedback_parts.append(f\"FAIL Total Denied: expected 32.00, found '{denied_val}' (numeric: {denied_num})\")\n\n    # 4. Final Status\n    status_val = find_key(summary, r'final\\s*status')\n    if status_val is not None and re.search(r'partially\\s*approved', str(status_val), re.IGNORECASE):\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Final Status: '{status_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Final Status: expected 'Partially Approved', found '{status_val}'\")\n\n    # 5. Manager Notified\n    notified_val = find_key(summary, r'manager\\s*notified')\n    if notified_val is not None and str(notified_val).strip().upper() == 'N':\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Manager Notified: '{notified_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Manager Notified: expected 'N', found '{notified_val}'\")\n\n    # 6. Comments\n    comment_val = find_key(summary, r'comments?')\n    expected_comment = \"T&E review complete. Expenses approved except where noted.\"\n    if comment_val is not None:\n        # Loose comparison: strip, case-insensitive, allow minor punctuation differences\n        c_clean = re.sub(r'\\s+', ' ', str(comment_val).strip().lower())\n        e_clean = re.sub(r'\\s+', ' ', expected_comment.strip().lower())\n        if c_clean == e_clean or e_clean in c_clean:\n            checks_passed += 1\n            feedback_parts.append(f\"PASS Comment: '{comment_val}'\")\n        else:\n            feedback_parts.append(f\"FAIL Comment: expected '{expected_comment}', found '{comment_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Comment: label 'Comment'/'Comments' not found in summary section\")\n\n    score = checks_passed / total_checks\n    passed = checks_passed == total_checks\n    feedback = f\"Carlos Mendez Report Summary: {checks_passed}/{total_checks} checks passed.\\n\" + \"\\n\".join(feedback_parts)\n    return {\"pass\": passed, \"score\": score, \"feedback\": feedback}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776176807994",
    "sort_order": 11,
    "rubric_text": "In the worksheet \"Priya Natarajan\" of `expense_reports.xlsx`, each of the following line-item rows must exist (identified by the Jira Key in column A) with the listed values in columns B through H. Row order is not graded. Every line item must be fully denied, and every Denial Reason must state or be equivalent to \"Missing business justification. Please submit personal reimbursement.\" Dollar amounts match on numeric value and ignore currency symbols or thousands separators.\n\n- **OPS-34**: Date 5/5/2026, Merchant \"Self drove (mileage)\", Amount \"200 miles\" or equivalent, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 145.00 (tolerance ±0.05).\n- **OPS-33**: Date 5/5/2026, Merchant \"Riverfront Deli\", Amount 16.59, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 16.59.\n- **OPS-35**: Date 5/6/2026, Merchant \"The Creamery Cafe\", Amount 21.40, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 21.40.\n- **OPS-36**: Date 5/7/2026, Merchant \"Canal Street Kitchen\", Amount 16.05, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 16.05.\n- **OPS-37**: Date 5/8/2026, Merchant \"The Landing Spot\", Amount 20.33, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 20.33.\n- **OPS-38**: Date \"5/5/2026 - 5/9/2026\", Merchant \"Serenity Hotel and Spa\", Amount 1471.96, Expense Category \"Staff Expense\", Approved Amount 0, Denied Amount 1471.96.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\nimport re\nimport datetime\n\ndef verify(workspace_path, external_services_path=None):\n    filepath = Path(workspace_path) / \"expense_reports.xlsx\"\n    if not filepath.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"File expense_reports.xlsx not found.\"}\n\n    wb = openpyxl.load_workbook(str(filepath))\n    sheet = None\n    for name in wb.sheetnames:\n        if \"priya\" in name.lower() and \"natarajan\" in name.lower():\n            sheet = wb[name]\n            break\n    if sheet is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"Worksheet 'Priya Natarajan' not found. Available sheets: {wb.sheetnames}\"}\n\n    rows = []\n    for row in sheet.iter_rows(min_row=1, values_only=False):\n        vals = [cell.value for cell in row]\n        rows.append(vals)\n\n    row_dict = {}\n    for r in rows:\n        key = str(r[0]).strip() if r[0] is not None else \"\"\n        if key:\n            row_dict[key] = r\n\n    def parse_num(val):\n        if val is None:\n            return None\n        if isinstance(val, (int, float)):\n            return float(val)\n        s = str(val).strip()\n        s = re.sub(r'[\\$,]', '', s)\n        try:\n            return float(s)\n        except:\n            return None\n\n    def normalize_str(val):\n        if val is None:\n            return \"\"\n        return str(val).strip().lower()\n\n    def parse_date_tuple(val):\n        if val is None:\n            return None\n        if isinstance(val, (datetime.datetime, datetime.date)):\n            return (val.month, val.day, val.year)\n        s = str(val).strip()\n        m = re.match(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', s)\n        if m:\n            return (int(m.group(1)), int(m.group(2)), int(m.group(3)))\n        m = re.match(r'(\\d{4})-(\\d{1,2})-(\\d{1,2})', s)\n        if m:\n            return (int(m.group(2)), int(m.group(3)), int(m.group(1)))\n        return None\n\n    def date_matches(actual_cell, expected_str):\n        exp_lower = expected_str.lower().strip()\n        if ' - ' in exp_lower or ' to ' in exp_lower:\n            parts = re.findall(r'(\\d{1,2})/(\\d{1,2})/(\\d{4})', exp_lower)\n            if len(parts) >= 2:\n                start = (int(parts[0][0]), int(parts[0][1]), int(parts[0][2]))\n                end = (int(parts[1][0]), int(parts[1][1]), int(parts[1][2]))\n                act_str = normalize_str(actual_cell)\n                # Accept a textual span mentioning both dates (slash or ISO form)\n                start_iso = f\"{start[2]}-{start[0]:02d}-{start[1]:02d}\"\n                end_iso = f\"{end[2]}-{end[0]:02d}-{end[1]:02d}\"\n                if (start_iso in act_str and end_iso in act_str):\n                    return True\n                if all(f\"{m}/{d}\" in act_str or f\"{m:02d}/{d:02d}\" in act_str\n                       for m, d, _ in (start, end)):\n                    return True\n                act_tuple = parse_date_tuple(actual_cell)\n                if act_tuple in (start, end):\n                    return True\n                return False\n        exp_tuple = parse_date_tuple(expected_str)\n        act_tuple = parse_date_tuple(actual_cell)\n        if exp_tuple and act_tuple and exp_tuple == act_tuple:\n            return True\n        act_str = normalize_str(actual_cell)\n        if exp_lower in act_str or act_str in exp_lower:\n            return True\n        return False\n\n    def merchant_matches(cell_val, expected):\n        if cell_val is None:\n            return False\n        return normalize_str(cell_val) == normalize_str(expected) or normalize_str(expected) in normalize_str(cell_val) or normalize_str(cell_val) in normalize_str(expected)\n\n    def category_matches(cell_val, expected):\n        if cell_val is None:\n            return False\n        return normalize_str(expected) in normalize_str(cell_val) or normalize_str(cell_val) in normalize_str(expected)\n\n    def denial_reason_ok(cell_val):\n        if cell_val is None:\n            return False\n        s = normalize_str(cell_val)\n        if \"business justification\" in s or (\"justification\" in s and (\"missing\" in s or \"no \" in s)):\n            return True\n        if \"personal\" in s and (\"expense\" in s or \"reimbursement\" in s or \"claim\" in s):\n            return True\n        return False\n\n    expected_rows = [\n        {\n            \"key\": \"OPS-34\",\n            \"date\": \"5/5/2026\",\n            \"merchant\": \"Self drove (mileage)\",\n            \"amount_check\": lambda v: (\"200\" in str(v) and (\"mile\" in str(v).lower() or \"mi\" in str(v).lower())) or (parse_num(v) == 200 if v is not None else False),\n            \"amount_display\": \"200 miles or equivalent\",\n            \"category\": \"Staff Expense\",\n            \"approved\": 0,\n            \"denied\": 145.00,\n            \"denied_tol\": 0.10\n        },\n        {\n            \"key\": \"OPS-33\",\n            \"date\": \"5/5/2026\",\n            \"merchant\": \"Riverfront Deli\",\n            \"amount_check\": lambda v: abs((parse_num(v) or -999) - 16.59) < 0.05,\n            \"amount_display\": \"16.59\",\n            \"category\": \"Staff Expense\",\n            \"approved\": 0,\n            \"denied\": 16.59,\n            \"denied_tol\": 0.05\n        },\n        {\n            \"key\": \"OPS-35\",\n            \"date\": \"5/6/2026\",\n            \"merchant\": \"The Creamery Cafe\",\n            \"amount_check\": lambda v: abs((parse_num(v) or -999) - 21.40) < 0.05,\n            \"amount_display\": \"21.40\",\n            \"category\": \"Staff Expense\",\n            \"approved\": 0,\n            \"denied\": 21.40,\n            \"denied_tol\": 0.05\n        },\n        {\n            \"key\": \"OPS-36\",\n            \"date\": \"5/7/2026\",\n            \"merchant\": \"Canal Street Kitchen\",\n            \"amount_check\": lambda v: abs((parse_num(v) or -999) - 16.05) < 0.05,\n            \"amount_display\": \"16.05\",\n            \"category\": \"Staff Expense\",\n            \"approved\": 0,\n            \"denied\": 16.05,\n            \"denied_tol\": 0.05\n        },\n        {\n            \"key\": \"OPS-37\",\n            \"date\": \"5/8/2026\",\n            \"merchant\": \"The Landing Spot\",\n            \"amount_check\": lambda v: abs((parse_num(v) or -999) - 20.33) < 0.05,\n            \"amount_display\": \"20.33\",\n            \"category\": \"Staff Expense\",\n            \"approved\": 0,\n            \"denied\": 20.33,\n            \"denied_tol\": 0.05\n        },\n        {\n            \"key\": \"OPS-38\",\n            \"date\": \"5/5/2026 - 5/9/2026\",\n            \"merchant\": \"Serenity Hotel and Spa\",\n            \"amount_check\": lambda v: abs((parse_num(v) or -999) - 1471.96) < 1.0,\n            \"amount_display\": \"1471.96\",\n            \"category\": \"Staff Expense\",\n            \"approved\": 0,\n            \"denied\": 1471.96,\n            \"denied_tol\": 1.0\n        }\n    ]\n\n    issues = []\n    passed_count = 0\n    total_checks = len(expected_rows)\n\n    for exp in expected_rows:\n        jira_key = exp[\"key\"]\n        found_row = None\n        for rk, rv in row_dict.items():\n            if jira_key.lower() in rk.lower() or rk.lower() in jira_key.lower():\n                found_row = rv\n                break\n        \n        if found_row is None:\n            issues.append(f\"Row with Jira Key '{jira_key}' not found.\")\n            continue\n\n        row_issues = []\n\n        date_val = found_row[1] if len(found_row) > 1 else None\n        if not date_matches(date_val, exp[\"date\"]):\n            row_issues.append(f\"Date: expected '{exp['date']}', got '{date_val}'\")\n\n        merchant_val = found_row[2] if len(found_row) > 2 else None\n        if not merchant_matches(merchant_val, exp[\"merchant\"]):\n            merchant_s = normalize_str(merchant_val)\n            exp_words = [w.lower() for w in exp[\"merchant\"].split() if len(w) > 2]\n            matching_words = sum(1 for w in exp_words if w in merchant_s)\n            if matching_words < len(exp_words) * 0.5:\n                row_issues.append(f\"Merchant: expected '{exp['merchant']}', got '{merchant_val}'\")\n\n        amount_val = found_row[3] if len(found_row) > 3 else None\n        if not exp[\"amount_check\"](amount_val):\n            row_issues.append(f\"Amount: expected {exp['amount_display']}, got '{amount_val}'\")\n\n        cat_val = found_row[4] if len(found_row) > 4 else None\n        if not category_matches(cat_val, exp[\"category\"]):\n            row_issues.append(f\"Category: expected '{exp['category']}', got '{cat_val}'\")\n\n        approved_val = parse_num(found_row[5]) if len(found_row) > 5 else None\n        if approved_val is None or abs(approved_val - exp[\"approved\"]) > 0.05:\n            row_issues.append(f\"Approved Amount: expected {exp['approved']}, got '{found_row[5] if len(found_row) > 5 else None}'\")\n\n        denied_val = parse_num(found_row[6]) if len(found_row) > 6 else None\n        if denied_val is None or abs(denied_val - exp[\"denied\"]) > exp[\"denied_tol\"]:\n            row_issues.append(f\"Denied Amount: expected {exp['denied']} (±{exp['denied_tol']}), got '{found_row[6] if len(found_row) > 6 else None}'\")\n\n        reason_val = found_row[7] if len(found_row) > 7 else None\n        if not denial_reason_ok(reason_val):\n            row_issues.append(f\"Denial Reason: expected equivalent to 'Missing business justification. Please submit personal reimbursement.', got '{reason_val}'\")\n\n        if row_issues:\n            issues.append(f\"Row '{jira_key}': \" + \"; \".join(row_issues))\n        else:\n            passed_count += 1\n\n    score = passed_count / total_checks if total_checks > 0 else 0.0\n    passed = passed_count == total_checks\n\n    if passed:\n        feedback = f\"All {total_checks} line-item rows in 'Priya Natarajan' worksheet verified successfully.\"\n    else:\n        feedback = f\"{passed_count}/{total_checks} rows passed. Issues:\\n\" + \"\\n\".join(issues)\n\n    return {\"pass\": passed, \"score\": score, \"feedback\": feedback}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776177197899",
    "sort_order": 12,
    "rubric_text": "The \"Priya Natarajan\" worksheet in `expense_reports.xlsx` must contain a Report Summary section after the last line item with the following labels in column A and the listed values in column B (row order within the section is not graded). Monetary values may appear as numbers or as the value returned by a SUM formula over the Approved Amount or Denied Amount column; currency symbols and thousands separators are ignored.\n\n- Label **\"Jira Key\"** with value **OPS-39**.\n- Label **\"Total Approved\"** with value **0**.\n- Label **\"Total Denied\"** with value **1691.33** (tolerance ±1.00).\n- Label **\"Final Status\"** with value **\"Denied\"**.\n- Label **\"Manager Notified\"** with value **\"Y\"**.\n- Label **\"Comment\"** (or **\"Comments\"**) with text that clearly states the expense report is denied and cites a denied total within 1.00 of 1691.33; an acceptable exact value is \"T&E review complete. Denied expenses: 1691.33 dollars.\"",
    "verifier_code": "from pathlib import Path\nimport re\nimport openpyxl\n\n\ndef verify(workspace_path, external_services_path=None):\n    filepath = Path(workspace_path) / \"expense_reports.xlsx\"\n    if not filepath.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"File expense_reports.xlsx not found.\"}\n\n    wb = openpyxl.load_workbook(str(filepath), data_only=True)\n\n    # Find the worksheet for Priya Natarajan (case-insensitive match)\n    ws = None\n    for name in wb.sheetnames:\n        if name.strip().lower() == \"priya natarajan\":\n            ws = wb[name]\n            break\n    if ws is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"Worksheet 'Priya Natarajan' not found. Found sheets: {wb.sheetnames}\"}\n\n    # Build a dict of label -> value from the Report Summary section\n    # We scan column A for labels and column B for values\n    # The Report Summary section starts after a row containing \"Report Summary\" in column A\n    summary_started = False\n    summary = {}  # normalized_label -> raw_value\n    for row in ws.iter_rows(min_row=1, max_row=ws.max_row, min_col=1, max_col=2):\n        cell_a = row[0].value\n        cell_b = row[1].value if len(row) > 1 else None\n        if cell_a is not None and str(cell_a).strip().lower() == \"report summary\":\n            summary_started = True\n            continue\n        if summary_started and cell_a is not None:\n            label = str(cell_a).strip()\n            summary[label.lower()] = cell_b\n\n    if not summary_started:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"Could not find 'Report Summary' header row in the Priya Natarajan worksheet.\"}\n\n    feedback_parts = []\n    checks_passed = 0\n    total_checks = 6\n\n    def get_val(key_variants):\n        for k in key_variants:\n            if k.lower() in summary:\n                return summary[k.lower()]\n        return None\n\n    def parse_number(val):\n        \"\"\"Try to extract a numeric value from val.\"\"\"\n        if val is None:\n            return None\n        if isinstance(val, (int, float)):\n            return float(val)\n        s = str(val).strip()\n        # Remove currency symbols, commas, spaces, dollar word\n        s = re.sub(r'[\\$,€£]', '', s)\n        s = s.strip()\n        try:\n            return float(s)\n        except ValueError:\n            return None\n\n    # 1. Jira Key\n    jira_val = get_val([\"Jira Key\", \"jira key\", \"JIRA Key\"])\n    if jira_val is not None and \"natarajan-2026-05-11\" in str(jira_val).strip().lower():\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Jira Key: found '{jira_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Jira Key: expected 'OPS-39', found '{jira_val}'\")\n\n    # 2. Total Approved == 0\n    ta_val = get_val([\"Total Approved\"])\n    ta_num = parse_number(ta_val)\n    if ta_num is not None and abs(ta_num - 0) <= 1.0:\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Total Approved: {ta_num}\")\n    else:\n        feedback_parts.append(f\"FAIL Total Approved: expected 0, found '{ta_val}' (parsed: {ta_num})\")\n\n    # 3. Total Denied == 1691.33 +/- 1.00\n    td_val = get_val([\"Total Denied\"])\n    td_num = parse_number(td_val)\n    if td_num is not None and abs(td_num - 1691.33) <= 1.00:\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Total Denied: {td_num}\")\n    else:\n        feedback_parts.append(f\"FAIL Total Denied: expected 1691.33 (±1.00), found '{td_val}' (parsed: {td_num})\")\n\n    # 4. Final Status == Denied\n    fs_val = get_val([\"Final Status\"])\n    if fs_val is not None and str(fs_val).strip().lower() == \"denied\":\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Final Status: '{fs_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Final Status: expected 'Denied', found '{fs_val}'\")\n\n    # 5. Manager Notified == Y\n    mn_val = get_val([\"Manager Notified\"])\n    if mn_val is not None and str(mn_val).strip().upper().startswith(\"Y\"):\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Manager Notified: '{mn_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Manager Notified: expected 'Y', found '{mn_val}'\")\n\n    # 6. Comment / Comments — must mention denied and cite amount ~1691.33\n    cm_val = get_val([\"Comment\", \"Comments\"])\n    cm_pass = False\n    if cm_val is not None:\n        cm_str = str(cm_val).lower()\n        if \"denied\" in cm_str or \"deny\" in cm_str or \"denial\" in cm_str:\n            # Look for a number near 1691.33\n            nums = re.findall(r'\\d[\\d,]*\\.?\\d*', cm_str)\n            for n in nums:\n                try:\n                    nf = float(n.replace(',', ''))\n                    if abs(nf - 1691.33) <= 1.00:\n                        cm_pass = True\n                        break\n                except ValueError:\n                    pass\n    if cm_pass:\n        checks_passed += 1\n        feedback_parts.append(f\"PASS Comment: '{cm_val}'\")\n    else:\n        feedback_parts.append(f\"FAIL Comment: expected text stating denial with amount ~1691.33, found '{cm_val}'\")\n\n    score = checks_passed / total_checks\n    passed = checks_passed == total_checks\n    feedback = f\"{checks_passed}/{total_checks} checks passed. \" + \"; \".join(feedback_parts)\n    return {\"pass\": passed, \"score\": score, \"feedback\": feedback}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776177271321",
    "sort_order": 13,
    "rubric_text": "In `slack_data.json`, there must be no message in channel #finance-approvals referencing \"Rahimi\" that contains the string \"OPS-HR-TE-Rahimi\" or otherwise flags Leila Rahimi's expense report to a manager. Rahimi's report is Partially Approved with denials under $200, so per SOP section 6.2 Step 9, no manager notification is required.",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n    # We need to check slack_data.json for any messages in #finance-approvals\n    # referencing Rahimi's expense report. Per SOP 6.2 Step 9, no manager\n    # notification is required since denials are under $200.\n    \n    slack_path = None\n    \n    # Check external_services_path first\n    if external_services_path:\n        candidate = Path(external_services_path) / \"slack_data.json\"\n        if candidate.exists():\n            slack_path = candidate\n    \n    # Also check workspace_path\n    if slack_path is None:\n        candidate = Path(workspace_path) / \"slack_data.json\"\n        if candidate.exists():\n            slack_path = candidate\n    \n    # Search more broadly if not found\n    if slack_path is None:\n        for p in Path(workspace_path).rglob(\"slack_data.json\"):\n            slack_path = p\n            break\n        if slack_path is None and external_services_path:\n            for p in Path(external_services_path).rglob(\"slack_data.json\"):\n                slack_path = p\n                break\n    \n    if slack_path is None:\n        return {\n            \"pass\": True,\n            \"score\": 1.0,\n            \"feedback\": \"No slack_data.json found anywhere. Since no Slack messages exist, no manager notification about Rahimi was sent. PASS.\"\n        }\n    \n    try:\n        with open(slack_path, 'r') as f:\n            slack_data = json.load(f)\n    except Exception as e:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": f\"Could not read slack_data.json: {e}\"}\n    \n    # Find the #finance-approvals channel ID\n    channels = slack_data.get(\"channels\", {})\n    finance_approvals_ids = []\n    for ch_id, ch_info in channels.items():\n        ch_name = \"\"\n        if isinstance(ch_info, dict):\n            ch_name = ch_info.get(\"name\", \"\")\n        elif isinstance(ch_info, str):\n            ch_name = ch_info\n        if ch_name.lower().replace(\"#\", \"\") == \"finance-approvals\":\n            finance_approvals_ids.append(ch_id)\n    \n    # If no finance-approvals channel found, check all channels as a fallback\n    # (be generous - maybe the channel name is slightly different)\n    if not finance_approvals_ids:\n        for ch_id, ch_info in channels.items():\n            ch_name = \"\"\n            if isinstance(ch_info, dict):\n                ch_name = ch_info.get(\"name\", \"\")\n            elif isinstance(ch_info, str):\n                ch_name = ch_info\n            if \"finance\" in ch_name.lower() and \"approv\" in ch_name.lower():\n                finance_approvals_ids.append(ch_id)\n    \n    messages_data = slack_data.get(\"messages\", {})\n    \n    # Collect messages from the finance-approvals channel(s)\n    flagged_messages = []\n    \n    rahimi_patterns = [\n        r'OPS-HR-TE-Rahimi',\n        r'Rahimi',\n        r'Leila\\s+Rahimi',\n    ]\n    \n    def check_text(text, source_desc):\n        if not isinstance(text, str):\n            return\n        for pattern in rahimi_patterns:\n            if re.search(pattern, text, re.IGNORECASE):\n                flagged_messages.append(f\"Found '{pattern}' match in {source_desc}: {text[:300]}\")\n                break  # one match per text is enough\n    \n    def search_messages(msg_list, channel_desc):\n        if not isinstance(msg_list, list):\n            return\n        for i, msg in enumerate(msg_list):\n            if isinstance(msg, dict):\n                text = msg.get(\"text\", \"\")\n                check_text(text, f\"{channel_desc} message[{i}]\")\n                # Also check attachments or blocks\n                for key in [\"attachments\", \"blocks\"]:\n                    val = msg.get(key, [])\n                    if isinstance(val, list):\n                        for j, item in enumerate(val):\n                            if isinstance(item, dict):\n                                for field in [\"text\", \"fallback\", \"pretext\", \"title\"]:\n                                    check_text(item.get(field, \"\"), f\"{channel_desc} message[{i}].{key}[{j}].{field}\")\n            elif isinstance(msg, str):\n                check_text(msg, f\"{channel_desc} message[{i}]\")\n    \n    if finance_approvals_ids:\n        for ch_id in finance_approvals_ids:\n            msg_list = messages_data.get(ch_id, [])\n            search_messages(msg_list, f\"#finance-approvals ({ch_id})\")\n    else:\n        # No finance-approvals channel found; check all channels as a broader search\n        for ch_id, msg_list in messages_data.items():\n            search_messages(msg_list, f\"channel {ch_id}\")\n    \n    if flagged_messages:\n        details = \"\\n\".join(flagged_messages)\n        return {\n            \"pass\": False,\n            \"score\": 0.0,\n            \"feedback\": f\"FAIL: Found Slack messages in #finance-approvals (or relevant channels) referencing Rahimi's expense report. Per SOP 6.2 Step 9, no manager notification should have been sent since denials are under $200.\\nDetails:\\n{details}\"\n        }\n    \n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"PASS: No Slack messages found in #finance-approvals referencing Rahimi or her expense report (OPS-HR-TE-Rahimi). No manager notification was sent, which is correct per SOP 6.2 Step 9 since denials are under $200.\"\n    }\n",
    "criterion_type": "incorrect_behavior"
  },
  {
    "id": "rubric_1776177277018",
    "sort_order": 14,
    "rubric_text": "In `slack_data.json`, there must be a Slack message posted to the channel named `finance-approvals` (channel id `C001`) that addresses Priya Natarajan's reporting manager `l_zahra` and contains all of the following:\n\n1. The phrase \"T&E Review Flag\"\n2. The employee name \"Priya Natarajan\"\n3. A date range covering 5/5/2026 through 5/10/2026 (any equivalent date format is accepted)\n4. A Total Amount Approved of 0\n5. A Total Amount Denied value within 1.00 of 1691.33\n6. The merchants \"Serenity Hotel and Spa\", \"Riverfront Deli\", \"The Creamery Cafe\", \"Canal Street Kitchen\", and \"The Landing Spot\", and a \"mileage reimbursement\" entry for the mileage line per Template B-2\n7. A Details line indicating that the expenses were submitted without business justification\n8. An Action Taken line indicating that expense report OPS-39 has been denied\n\nCurrency symbols and thousands separators are ignored when comparing amounts.",
    "verifier_code": "import json\nimport os\nimport re\nimport datetime\n\ndef verify(workspace_path, external_services_path=None):\n    # --- Locate slack_data.json, preferring post-run state --------------\n    preferred_subpaths = [\n        \"external_services_final/slack_data.json\",\n        \"external_services/slack_data.json\",\n        \"workspace/slack_data.json\",\n        \"draft/external_services_final/slack_data.json\",\n        \"draft/workspace/slack_data.json\",\n        \"draft/slack_data.json\",\n        \"slack_data.json\",\n    ]\n    candidates = [os.path.join(workspace_path, p) for p in preferred_subpaths]\n    parent = os.path.dirname(workspace_path.rstrip(\"/\"))\n    if parent:\n        candidates.extend(os.path.join(parent, p) for p in preferred_subpaths)\n\n    slack_path = next((c for c in candidates if os.path.isfile(c)), None)\n\n    # Recursive fallback — but skip _initial, prefer _final, then anything else\n    if slack_path is None:\n        hits = []\n        roots = [workspace_path] + ([parent] if parent else [])\n        for root in roots:\n            if not os.path.isdir(root):\n                continue\n            for dirpath, _, filenames in os.walk(root):\n                if \"slack_data.json\" in filenames:\n                    hits.append(os.path.join(dirpath, \"slack_data.json\"))\n\n        def _rank(p):\n            lp = p.lower()\n            if \"_initial\" in lp or \"initial\" in os.path.basename(os.path.dirname(lp)):\n                return 3\n            if \"_final\" in lp or \"final\" in os.path.basename(os.path.dirname(lp)):\n                return 0\n            if \"workspace\" in lp:\n                return 1\n            return 2\n\n        hits.sort(key=_rank)\n        if hits:\n            slack_path = hits[0]\n\n    if slack_path is None:\n        return {\"pass\": False, \"score\": 0.0,\n                \"feedback\": f\"Could not locate slack_data.json under {workspace_path}\"}\n\n    try:\n        with open(slack_path, \"r\") as f:\n            slack_data = json.load(f)\n    except Exception as e:\n        return {\"pass\": False, \"score\": 0.0,\n                \"feedback\": f\"Could not load slack_data.json at {slack_path}: {e}\"}\n\n    # --- Figure out which channel IDs correspond to #finance-approvals ---\n    target_ch_ids = {\"C001\"}\n\n    def _register_channel(ch_id, ch_name):\n        if ch_id:\n            if ch_id == \"C001\":\n                target_ch_ids.add(ch_id)\n            if ch_name and \"finance-approvals\" in ch_name.lower().replace(\"#\", \"\"):\n                target_ch_ids.add(ch_id)\n\n    channels_field = slack_data.get(\"channels\") if isinstance(slack_data, dict) else None\n    if isinstance(channels_field, dict):\n        for ch_id, ch in channels_field.items():\n            ch_name = ch.get(\"name\", \"\") if isinstance(ch, dict) else \"\"\n            _register_channel(ch_id, ch_name)\n    elif isinstance(channels_field, list):\n        for ch in channels_field:\n            if isinstance(ch, dict):\n                _register_channel(ch.get(\"id\") or ch.get(\"channel_id\"),\n                                   ch.get(\"name\", \"\"))\n\n    # --- Collect messages from every plausible shape --------------------\n    def _msg_in_target_channel(m):\n        if not isinstance(m, dict):\n            return False\n        ch = (m.get(\"channel_id\") or m.get(\"channel\")\n              or m.get(\"channel_name\") or m.get(\"conversation_id\"))\n        if ch is None:\n            return False\n        if ch in target_ch_ids:\n            return True\n        if isinstance(ch, str) and \"finance-approvals\" in ch.lower().replace(\"#\", \"\"):\n            return True\n        return False\n\n    messages = []\n\n    msgs_field = slack_data.get(\"messages\") if isinstance(slack_data, dict) else None\n    if isinstance(msgs_field, dict):\n        for ch_id, ms in msgs_field.items():\n            if ch_id in target_ch_ids and isinstance(ms, list):\n                messages.extend(ms)\n            elif isinstance(ch_id, str) and \"finance-approvals\" in ch_id.lower().replace(\"#\", \"\"):\n                if isinstance(ms, list):\n                    messages.extend(ms)\n    elif isinstance(msgs_field, list):\n        for m in msgs_field:\n            if _msg_in_target_channel(m):\n                messages.append(m)\n\n    if isinstance(channels_field, dict):\n        for ch_id, ch in channels_field.items():\n            if not isinstance(ch, dict):\n                continue\n            if ch_id not in target_ch_ids and \\\n               \"finance-approvals\" not in str(ch.get(\"name\", \"\")).lower().replace(\"#\", \"\"):\n                continue\n            for key in (\"messages\", \"msgs\", \"posts\", \"history\"):\n                nested = ch.get(key)\n                if isinstance(nested, list):\n                    messages.extend(nested)\n    elif isinstance(channels_field, list):\n        for ch in channels_field:\n            if not isinstance(ch, dict):\n                continue\n            ch_id = ch.get(\"id\") or ch.get(\"channel_id\")\n            ch_name = ch.get(\"name\", \"\")\n            if ch_id not in target_ch_ids and \\\n               \"finance-approvals\" not in str(ch_name).lower().replace(\"#\", \"\"):\n                continue\n            for key in (\"messages\", \"msgs\", \"posts\", \"history\"):\n                nested = ch.get(key)\n                if isinstance(nested, list):\n                    messages.extend(nested)\n\n    if not messages:\n        found = []\n\n        def _walk(obj):\n            if isinstance(obj, dict):\n                text = obj.get(\"text\") or obj.get(\"message\") or obj.get(\"body\")\n                if isinstance(text, str) and text.strip():\n                    if _msg_in_target_channel(obj):\n                        found.append(obj)\n                    elif \"t&e review flag\" in text.lower() and \"natarajan\" in text.lower():\n                        found.append(obj)\n                for v in obj.values():\n                    _walk(v)\n            elif isinstance(obj, list):\n                for v in obj:\n                    _walk(v)\n\n        _walk(slack_data)\n        messages.extend(found)\n\n    if not messages:\n        return {\n            \"pass\": False, \"score\": 0.0,\n            \"feedback\": (\n                f\"No messages found in finance-approvals channel. \"\n                f\"Loaded slack_data.json from {slack_path}. \"\n                f\"Target channel IDs: {sorted(target_ch_ids)}. \"\n                f\"Top-level keys: {list(slack_data.keys()) if isinstance(slack_data, dict) else type(slack_data).__name__}\"\n            ),\n        }\n\n    # --- Scoring ---------------------------------------------------------\n    def score_message(text):\n        results = []\n        t = text.lower()\n\n        results.append((\"T&E Review Flag\",\n                        \"t&e review flag\" in t or \"t & e review flag\" in t))\n\n        results.append((\"Priya Natarajan\",\n                        \"priya natarajan\" in t or \"natarajan\" in t))\n\n        results.append((\"manager 'l_zahra'\",\n                        \"l_zahra\" in t or \"@l_zahra\" in t))\n\n        start_patterns = [r\"5/5/2026\", r\"05/05/2026\", r\"2026-05-05\"]\n        end_patterns = [\n            r\"5/9/2026\", r\"05/09/2026\", r\"2026-05-09\",\n            r\"5/10/2026\", r\"05/10/2026\", r\"2026-05-10\",\n        ]\n        start_ok = any(re.search(p, t) for p in start_patterns)\n        end_ok = any(re.search(p, t) for p in end_patterns)\n        results.append((\n            \"Date range covers 5/5/2026 through 5/9 or 5/10/2026\",\n            start_ok and end_ok,\n        ))\n\n        approved_match = re.search(\n            r\"(?:total\\s+(?:amount\\s+)?approved|amount\\s+approved)[^\\d\\-]*\\$?\\s*([\\d,]+\\.?\\d*)\",\n            t)\n        approved_ok = False\n        approved_val = None\n        if approved_match:\n            try:\n                approved_val = float(approved_match.group(1).replace(\",\", \"\"))\n                approved_ok = abs(approved_val - 0.0) < 1.0\n            except Exception:\n                approved_ok = False\n        results.append((f\"Total Amount Approved = 0 (got {approved_val})\",\n                        approved_ok))\n\n        denied_match = re.search(\n            r\"(?:total\\s+(?:amount\\s+)?denied|amount\\s+denied)[^\\d\\-]*\\$?\\s*([\\d,]+\\.?\\d*)\",\n            t)\n        denied_ok = False\n        denied_val = None\n        if denied_match:\n            try:\n                denied_val = float(denied_match.group(1).replace(\",\", \"\"))\n                denied_ok = abs(denied_val - 1691.33) < 1.0\n            except Exception:\n                denied_ok = False\n        results.append((\n            f\"Total Amount Denied within $1.00 of 1691.33 (got {denied_val})\",\n            denied_ok,\n        ))\n\n        results.append((\"Includes mileage line\",\n                        \"mileage\" in t))\n\n        biz = (\"business justification\" in t or \"business purpose\" in t\n               or \"calendar\" in t or \"no calendar\" in t)\n        action = (\"action taken\" in t and (\"denied\" in t or \"denial\" in t))\n        results.append((\n            \"Business justification + Action Taken (denial) present\",\n            biz and action,\n        ))\n\n        passes = sum(1 for _, ok in results if ok)\n        return passes, results\n\n    best = (0, [], None)\n    for msg in messages:\n        text = msg.get(\"text\") if isinstance(msg, dict) else str(msg)\n        if not text:\n            text = msg.get(\"message\") if isinstance(msg, dict) else \"\"\n        if not text:\n            continue\n        passes, results = score_message(text)\n        if passes > best[0]:\n            best = (passes, results, text)\n\n    total_checks = 8\n    score = best[0] / total_checks\n    feedback_lines = [\n        f\"Loaded slack_data.json from {slack_path}\",\n        f\"Best matching message scored {best[0]}/{total_checks}:\",\n    ]\n    for label, ok in best[1]:\n        feedback_lines.append(f\"{'PASS' if ok else 'FAIL'}: {label}\")\n    return {\n        \"pass\": best[0] == total_checks,\n        \"score\": score,\n        \"feedback\": \"\\n\".join(feedback_lines),\n    }\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1776177312019",
    "sort_order": 15,
    "rubric_text": "In `slack_data.json`, there must be no message in channel #finance-approvals referencing \"Rahimi\" that contains the string \"OPS-HR-TE-Mendez\" or otherwise flags Carlos Mendez's expense report to a manager. Mendez's report is Partially Approved with denials under $200, so per SOP section 6.2 Step 9, no manager notification is required.",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n    # We need to check slack_data.json for any messages in #finance-approvals\n    # referencing Mendez's expense report to a manager.\n    # Per SOP 6.2 Step 9, no manager notification is required since denials are under $200.\n\n    slack_path = None\n    candidates = []\n    if external_services_path:\n        candidates.append(Path(external_services_path) / \"slack_data.json\")\n        candidates.append(Path(external_services_path) / \"slack\" / \"slack_data.json\")\n    candidates.append(Path(workspace_path) / \"slack_data.json\")\n\n    for c in candidates:\n        if c.exists():\n            slack_path = c\n            break\n\n    if slack_path is None:\n        # No slack_data.json found; no messages were sent, so no manager notification for Mendez exists.\n        return {\"pass\": True, \"score\": 1.0, \"feedback\": \"No slack_data.json found; no Slack messages were sent, so no manager notification for Mendez exists. PASS.\"}\n\n    try:\n        with open(slack_path, 'r') as f:\n            slack_data = json.load(f)\n    except Exception as e:\n        return {\"pass\": True, \"score\": 1.0, \"feedback\": f\"Could not parse slack_data.json ({e}); assuming no problematic messages. PASS (lenient).\"}\n\n    # Find the channel ID for #finance-approvals\n    finance_channel_id = None\n    channels = slack_data.get(\"channels\", {})\n    if isinstance(channels, dict):\n        for cid, cinfo in channels.items():\n            if isinstance(cinfo, dict):\n                cname = cinfo.get(\"name\", \"\")\n                if cname.lower() == \"finance-approvals\":\n                    finance_channel_id = cid\n                    break\n\n    # Collect messages from #finance-approvals channel\n    finance_messages = []\n    messages_data = slack_data.get(\"messages\", {})\n\n    if finance_channel_id and isinstance(messages_data, dict):\n        channel_msgs = messages_data.get(finance_channel_id, [])\n        if isinstance(channel_msgs, list):\n            finance_messages.extend(channel_msgs)\n\n    # Also do a broader search across all channels in case channel naming varies\n    all_channel_messages = []\n    if isinstance(messages_data, dict):\n        for chan_id, msgs in messages_data.items():\n            if isinstance(msgs, list):\n                for msg in msgs:\n                    all_channel_messages.append((chan_id, msg))\n\n    # Helper to extract all text from a message object\n    def get_message_text(msg):\n        texts = []\n        if isinstance(msg, dict):\n            if \"text\" in msg:\n                texts.append(str(msg[\"text\"]))\n            # Check attachments, blocks, etc.\n            for key in [\"attachments\", \"blocks\"]:\n                if key in msg:\n                    items = msg[key]\n                    if isinstance(items, list):\n                        for item in items:\n                            if isinstance(item, dict):\n                                for subkey in [\"text\", \"fallback\", \"pretext\", \"title\"]:\n                                    if subkey in item:\n                                        texts.append(str(item[subkey]))\n        elif isinstance(msg, str):\n            texts.append(msg)\n        return \" \".join(texts)\n\n    # Check 1: Look for messages in #finance-approvals containing \"OPS-HR-TE-Mendez\" or referencing Mendez + Rahimi\n    mendez_jira_pattern = re.compile(r'OPS[-\\s]?HR[-\\s]?TE[-\\s]?Mendez', re.IGNORECASE)\n    flagged_texts = []\n\n    # Check finance-approvals channel specifically\n    for msg in finance_messages:\n        text = get_message_text(msg)\n        text_lower = text.lower()\n        # Check for Mendez Jira key\n        if mendez_jira_pattern.search(text):\n            flagged_texts.append(text)\n        # Check for Mendez mentioned alongside Rahimi (manager notification)\n        elif 'mendez' in text_lower and 'rahimi' in text_lower:\n            flagged_texts.append(text)\n\n    if flagged_texts:\n        return {\n            \"pass\": False,\n            \"score\": 0.0,\n            \"feedback\": f\"FAIL: Found {len(flagged_texts)} Slack message(s) in #finance-approvals containing 'OPS-HR-TE-Mendez' or referencing Mendez's expense report to manager Rahimi. Per SOP 6.2 Step 9, no manager notification should be sent since denials are under $200. Flagged messages: {flagged_texts[:3]}\"\n        }\n\n    # Check 2: Broader check across all channels for Mendez manager notifications\n    mendez_manager_notifications = []\n    for chan_id, msg in all_channel_messages:\n        text = get_message_text(msg)\n        text_lower = text.lower()\n        # Check for OPS-HR-TE-Mendez jira key combined with Rahimi or manager-like context\n        if mendez_jira_pattern.search(text) and ('rahimi' in text_lower or 'manager' in text_lower or 'notify' in text_lower or 'notification' in text_lower or 'escalat' in text_lower):\n            mendez_manager_notifications.append(text)\n        # Check for Mendez + Rahimi together suggesting a manager notification\n        elif 'mendez' in text_lower and 'rahimi' in text_lower and ('denied' in text_lower or 'denial' in text_lower or 'expense' in text_lower or 'report' in text_lower or 'review' in text_lower or 'notify' in text_lower or 'notification' in text_lower):\n            mendez_manager_notifications.append(text)\n\n    if mendez_manager_notifications:\n        return {\n            \"pass\": False,\n            \"score\": 0.0,\n            \"feedback\": f\"FAIL: Found Slack message(s) that appear to flag Carlos Mendez's expense report to manager Rahimi. Per SOP 6.2 Step 9, no notification is required since denials are under $200. Messages: {mendez_manager_notifications[:3]}\"\n        }\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"PASS: No Slack messages found in #finance-approvals containing 'OPS-HR-TE-Mendez' or flagging Mendez's expense report to manager Rahimi. This is correct per SOP 6.2 Step 9 since denials are under $200.\"\n    }\n",
    "criterion_type": "incorrect_behavior"
  }
]
