[
  {
    "id": "cd482ae3-5a98-473d-944b-90077a455e30",
    "sort_order": 0,
    "rubric_text": "File `Suspense_Reconciliation_March2026.xlsx` must exist in the workspace and contain columns: Support Found, Support Location, Conflict, Status, and Resolution Notes.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found in workspace'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = wb.active\n        headers = [str(cell.value).strip() if cell.value else '' for cell in ws[1]]\n        headers_lower = [h.lower() for h in headers]\n        required_cols = ['support found', 'support location', 'conflict', 'status', 'resolution notes']\n        missing = [col for col in required_cols if col not in headers_lower]\n        if missing:\n            return {'pass': False, 'score': 0.0, 'feedback': f'Missing required columns: {missing}. Found columns: {headers}'}\n        return {'pass': True, 'score': 1.0, 'feedback': f'File exists and contains all required columns: {required_cols}'}\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error reading file: {e}'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "294d7b7f-70c3-46e0-993b-71b9f0cecf2c",
    "sort_order": 1,
    "rubric_text": "In file `Suspense_Reconciliation_March2026.xlsx`, cleared items OPS-2 ($3,250), OPS-3 ($7,800), OPS-7 ($2,100), and OPS-8 ($15,000) must each show Status=FIN-100, Support Found=Y, Conflict=N, and non-empty Resolution Notes.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = wb.active\n        headers = [str(cell.value).strip().lower() if cell.value else '' for cell in ws[1]]\n        def col_idx(name):\n            for i, h in enumerate(headers):\n                if name.lower() in h:\n                    return i\n            return None\n        id_col = col_idx('susp') if col_idx('susp') is not None else col_idx('id') if col_idx('id') is not None else 0\n        # Try to find transaction ID column\n        id_col = None\n        for i, h in enumerate(headers):\n            if 'id' in h or 'susp' in h or 'transaction' in h or 'item' in h:\n                id_col = i\n                break\n        if id_col is None:\n            id_col = 0\n        status_col = None\n        for i, h in enumerate(headers):\n            if 'status' in h:\n                status_col = i\n                break\n        support_found_col = None\n        for i, h in enumerate(headers):\n            if 'support found' in h or (i > 0 and 'support' in h and 'found' in h):\n                support_found_col = i\n                break\n        if support_found_col is None:\n            for i, h in enumerate(headers):\n                if h == 'support found':\n                    support_found_col = i\n                    break\n        conflict_col = None\n        for i, h in enumerate(headers):\n            if 'conflict' in h:\n                conflict_col = i\n                break\n        notes_col = None\n        for i, h in enumerate(headers):\n            if 'resolution' in h or 'notes' in h:\n                notes_col = i\n                break\n        if any(c is None for c in [status_col, support_found_col, conflict_col, notes_col]):\n            return {'pass': False, 'score': 0.0, 'feedback': f'Could not find required columns. Headers: {headers}'}\n        target_ids = {'OPS-2', 'OPS-3', 'OPS-7', 'OPS-8'}\n        found = {}\n        for row in ws.iter_rows(min_row=2, values_only=True):\n            row_id = str(row[id_col]).strip() if row[id_col] else ''\n            if row_id in target_ids:\n                found[row_id] = row\n        failures = []\n        for tid in target_ids:\n            if tid not in found:\n                failures.append(f'{tid}: not found in workbook')\n                continue\n            row = found[tid]\n            status = str(row[status_col]).strip() if row[status_col] else ''\n            sf = str(row[support_found_col]).strip().upper() if row[support_found_col] else ''\n            conflict = str(row[conflict_col]).strip().upper() if row[conflict_col] else ''\n            notes = str(row[notes_col]).strip() if row[notes_col] else ''\n            if 'FIN-100' not in status.upper() and status.upper() != 'FIN-100':\n                failures.append(f'{tid}: Status={status} (expected FIN-100)')\n            if sf not in ('Y', 'YES', 'TRUE', '1'):\n                failures.append(f'{tid}: Support Found={sf} (expected Y)')\n            if conflict not in ('N', 'NO', 'FALSE', '0'):\n                failures.append(f'{tid}: Conflict={conflict} (expected N)')\n            if not notes or notes.lower() == 'none':\n                failures.append(f'{tid}: Resolution Notes is empty')\n        if failures:\n            return {'pass': False, 'score': 0.0, 'feedback': 'Failures: ' + '; '.join(failures)}\n        return {'pass': True, 'score': 1.0, 'feedback': f'All cleared items (OPS-2, OPS-3, OPS-7, OPS-8) show Status=FIN-100, Support Found=Y, Conflict=N, and non-empty Resolution Notes'}\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error: {e}'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "731b9a6e-3474-439c-95c7-5855d1a54007",
    "sort_order": 2,
    "rubric_text": "In file `Suspense_Reconciliation_March2026.xlsx`, OPS-3 ($7,800) and OPS-8 ($15,000) Resolution Notes must reference a Slack approval message.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = wb.active\n        headers = [str(cell.value).strip().lower() if cell.value else '' for cell in ws[1]]\n        id_col = None\n        for i, h in enumerate(headers):\n            if 'id' in h or 'susp' in h or 'transaction' in h or 'item' in h:\n                id_col = i\n                break\n        if id_col is None:\n            id_col = 0\n        notes_col = None\n        for i, h in enumerate(headers):\n            if 'resolution' in h or 'notes' in h:\n                notes_col = i\n                break\n        if notes_col is None:\n            return {'pass': False, 'score': 0.0, 'feedback': f'Could not find Resolution Notes column. Headers: {headers}'}\n        target_ids = {'OPS-3', 'OPS-8'}\n        found = {}\n        for row in ws.iter_rows(min_row=2, values_only=True):\n            row_id = str(row[id_col]).strip() if row[id_col] else ''\n            if row_id in target_ids:\n                found[row_id] = row\n        failures = []\n        slack_keywords = ['slack', 'approval', 'approved', '#finance', 'channel', 'message']\n        for tid in target_ids:\n            if tid not in found:\n                failures.append(f'{tid}: not found in workbook')\n                continue\n            row = found[tid]\n            notes = str(row[notes_col]).strip().lower() if row[notes_col] else ''\n            if not any(kw in notes for kw in slack_keywords):\n                failures.append(f'{tid}: Resolution Notes does not reference Slack approval. Notes: \"{notes}\"')\n        if failures:\n            return {'pass': False, 'score': 0.0, 'feedback': 'Failures: ' + '; '.join(failures)}\n        return {'pass': True, 'score': 1.0, 'feedback': 'OPS-3 and OPS-8 Resolution Notes reference Slack approval message'}\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error: {e}'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "753ac24a-0b95-46ad-9b11-a3a2dd24d630",
    "sort_order": 3,
    "rubric_text": "In file `Suspense_Reconciliation_March2026.xlsx`, exception items OPS-4, OPS-5, OPS-6, OPS-9, OPS-10, OPS-11, and OPS-12 must each show Status=EXCEPTION and must NOT show Status=FIN-100.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = wb.active\n        headers = [str(cell.value).strip().lower() if cell.value else '' for cell in ws[1]]\n        id_col = None\n        for i, h in enumerate(headers):\n            if 'id' in h or 'susp' in h or 'transaction' in h or 'item' in h:\n                id_col = i\n                break\n        if id_col is None:\n            id_col = 0\n        status_col = None\n        for i, h in enumerate(headers):\n            if 'status' in h:\n                status_col = i\n                break\n        if status_col is None:\n            return {'pass': False, 'score': 0.0, 'feedback': f'Could not find Status column. Headers: {headers}'}\n        target_ids = {'OPS-4', 'OPS-5', 'OPS-6', 'OPS-9', 'OPS-10', 'OPS-11', 'OPS-12'}\n        found = {}\n        for row in ws.iter_rows(min_row=2, values_only=True):\n            row_id = str(row[id_col]).strip() if row[id_col] else ''\n            if row_id in target_ids:\n                found[row_id] = row\n        failures = []\n        for tid in target_ids:\n            if tid not in found:\n                failures.append(f'{tid}: not found in workbook')\n                continue\n            row = found[tid]\n            status = str(row[status_col]).strip().upper() if row[status_col] else ''\n            if 'EXCEPTION' not in status:\n                failures.append(f'{tid}: Status={status} (expected EXCEPTION)')\n            if 'FIN-100' in status:\n                failures.append(f'{tid}: Status={status} must NOT be FIN-100')\n        if failures:\n            return {'pass': False, 'score': 0.0, 'feedback': 'Failures: ' + '; '.join(failures)}\n        return {'pass': True, 'score': 1.0, 'feedback': 'All exception items (OPS-4, OPS-5, OPS-6, OPS-9, OPS-10, OPS-11, OPS-12) show Status=EXCEPTION'}\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error: {e}'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "e23f7481-1554-4d6d-9055-770e06854a17",
    "sort_order": 4,
    "rubric_text": "In file Suspense_Reconciliation_March2026.xlsx, OPS-4 must show Support Found=Y, Conflict=N, and Resolution Notes indicating missing Slack approval. OPS-5 must show Support Found=N, Conflict=N, and Resolution Notes indicating missing PDF. OPS-11 must show Support Found=Y, Conflict=N, and Resolution Notes indicating no manager approval found in #finance-approvals. OPS-12 must show Support Found=Y, Conflict=N, and Resolution Notes indicating approval must be confirmed by a manager, not a junior analyst.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    # Try to find the file - be flexible\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        candidates = list(Path(workspace_path).glob('*Suspense*Reconciliation*March*2026*.xlsx'))\n        if not candidates:\n            candidates = list(Path(workspace_path).glob('*suspense*reconciliation*march*2026*.xlsx'))\n        if not candidates:\n            candidates = list(Path(workspace_path).glob('*Suspense*March*.xlsx'))\n        if not candidates:\n            candidates = list(Path(workspace_path).glob('*suspense*march*.xlsx'))\n        if not candidates:\n            candidates = list(Path(workspace_path).glob('*Suspense*.xlsx'))\n        if not candidates:\n            candidates = list(Path(workspace_path).glob('*suspense*.xlsx'))\n        if not candidates:\n            candidates = list(Path(workspace_path).glob('*.xlsx'))\n        if candidates:\n            xlsx_path = candidates[0]\n        else:\n            return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found in workspace'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = None\n        for sheet_name in wb.sheetnames:\n            candidate_ws = wb[sheet_name]\n            first_row = list(candidate_ws.iter_rows(min_row=1, max_row=1, values_only=True))\n            if not first_row:\n                continue\n            candidate_headers = [str(cell).strip().lower() if cell else '' for cell in first_row[0]]\n            has_support = any('support' in h for h in candidate_headers)\n            has_conflict = any('conflict' in h for h in candidate_headers)\n            has_notes = any('resolution' in h or 'notes' in h for h in candidate_headers)\n            if has_support and has_conflict and has_notes:\n                ws = candidate_ws\n                break\n        if ws is None:\n            ws = wb.active\n\n        first_row = list(ws.iter_rows(min_row=1, max_row=1, values_only=True))\n        if not first_row or not first_row[0]:\n            return {'pass': False, 'score': 0.0, 'feedback': 'No headers found in first row'}\n        headers = [str(cell).strip().lower() if cell else '' for cell in first_row[0]]\n\n        id_col = None\n        for i, h in enumerate(headers):\n            if 'susp' in h or h == 'id' or 'item id' in h or 'item_id' in h:\n                id_col = i\n                break\n        if id_col is None:\n            for i, h in enumerate(headers):\n                if 'item' in h or 'transaction' in h or 'entry' in h or 'ref' in h:\n                    id_col = i\n                    break\n        if id_col is None:\n            id_col = 0\n\n        support_found_col = None\n        for i, h in enumerate(headers):\n            if 'support found' in h or ('support' in h and 'found' in h):\n                support_found_col = i\n                break\n        if support_found_col is None:\n            for i, h in enumerate(headers):\n                if 'support' in h and 'location' not in h and 'loc' not in h:\n                    support_found_col = i\n                    break\n\n        support_location_col = None\n        for i, h in enumerate(headers):\n            if 'support location' in h or ('support' in h and 'location' in h) or ('support' in h and 'loc' in h):\n                support_location_col = i\n                break\n\n        conflict_col = None\n        for i, h in enumerate(headers):\n            if 'conflict' in h:\n                conflict_col = i\n                break\n\n        status_col = None\n        for i, h in enumerate(headers):\n            if h == 'status' or 'status' in h:\n                status_col = i\n                break\n\n        notes_col = None\n        for i, h in enumerate(headers):\n            if 'resolution' in h and 'note' in h:\n                notes_col = i\n                break\n        if notes_col is None:\n            for i, h in enumerate(headers):\n                if 'resolution' in h:\n                    notes_col = i\n                    break\n        if notes_col is None:\n            for i, h in enumerate(headers):\n                if 'notes' in h or 'note' in h:\n                    notes_col = i\n                    break\n        if notes_col is None:\n            for i, h in enumerate(headers):\n                if 'comment' in h or 'remark' in h:\n                    notes_col = i\n                    break\n\n        missing_cols = []\n        if support_found_col is None:\n            missing_cols.append('Support Found')\n        if conflict_col is None:\n            missing_cols.append('Conflict')\n        if notes_col is None:\n            missing_cols.append('Resolution Notes')\n        if missing_cols:\n            return {'pass': False, 'score': 0.0, 'feedback': f'Missing required columns: {missing_cols}. Found headers: {headers}'}\n\n        rows = {}\n        for row in ws.iter_rows(min_row=2, values_only=True):\n            if row is None:\n                continue\n            row_id = ''\n            if id_col < len(row) and row[id_col]:\n                cell_str = str(row[id_col]).strip().upper()\n                if 'SUSP' in cell_str:\n                    import re\n                    match = re.search(r'SUSP-?\\d+', cell_str)\n                    if match:\n                        row_id = match.group(0)\n                        row_id = row_id.replace('SUSP', 'SUSP-').replace('SUSP--', 'SUSP-')\n            if not row_id:\n                for cell_val in row:\n                    if cell_val:\n                        cell_str = str(cell_val).strip().upper()\n                        if 'SUSP' in cell_str:\n                            import re\n                            match = re.search(r'SUSP-?\\d+', cell_str)\n                            if match:\n                                row_id = match.group(0)\n                                row_id = row_id.replace('SUSP', 'SUSP-').replace('SUSP--', 'SUSP-')\n                                break\n            if row_id in ('OPS-4', 'OPS-5', 'OPS-11', 'OPS-12'):\n                rows[row_id] = row\n\n        failures = []\n        details = []\n\n        def check_yn(value, expected_yes):\n            \"\"\"Strict check: only Y/Yes count as yes; only N/No count as no.\"\"\"\n            val = str(value).strip().upper() if value is not None else ''\n            if expected_yes:\n                return val in ('Y', 'YES')\n            else:\n                return val in ('N', 'NO')\n\n        def safe_get(row, col):\n            if col is not None and col < len(row):\n                return row[col]\n            return None\n\n        # OPS-4\n        if 'OPS-4' not in rows:\n            failures.append('OPS-4: row not found in spreadsheet')\n        else:\n            row = rows['OPS-4']\n            sf_val = safe_get(row, support_found_col)\n            conflict_val = safe_get(row, conflict_col)\n            notes_val = safe_get(row, notes_col)\n            sf = str(sf_val).strip().upper() if sf_val else ''\n            conflict = str(conflict_val).strip().upper() if conflict_val else ''\n            notes = str(notes_val).strip().lower() if notes_val else ''\n            if not check_yn(sf_val, True):\n                failures.append(f'OPS-4: Support Found=\"{sf}\" (expected Y)')\n            if not check_yn(conflict_val, False):\n                failures.append(f'OPS-4: Conflict=\"{conflict}\" (expected N)')\n            slack_keywords = ['slack', '#finance', 'channel', 'message']\n            approval_keywords = ['approval', 'approved', 'authorize', 'authoriz', 'confirm']\n            missing_keywords = ['missing', 'no ', 'not ', 'absent', 'lack', 'without', 'pending', 'needed', 'unable', 'could not', \"couldn't\", 'unavailable']\n            has_slack = any(kw in notes for kw in slack_keywords)\n            has_approval = any(kw in notes for kw in approval_keywords)\n            has_missing = any(kw in notes for kw in missing_keywords)\n            if not (has_slack or (has_approval and has_missing)):\n                failures.append(f'OPS-4: Resolution Notes does not indicate missing Slack approval: \"{notes[:120]}\"')\n            else:\n                details.append(f'OPS-4: OK (SF={sf}, Conflict={conflict}, Notes=\"{notes[:80]}\")')\n\n        # OPS-5\n        if 'OPS-5' not in rows:\n            failures.append('OPS-5: row not found in spreadsheet')\n        else:\n            row = rows['OPS-5']\n            sf_val = safe_get(row, support_found_col)\n            conflict_val = safe_get(row, conflict_col)\n            notes_val = safe_get(row, notes_col)\n            sf = str(sf_val).strip().upper() if sf_val else ''\n            conflict = str(conflict_val).strip().upper() if conflict_val else ''\n            notes = str(notes_val).strip().lower() if notes_val else ''\n            if not check_yn(sf_val, False):\n                failures.append(f'OPS-5: Support Found=\"{sf}\" (expected N)')\n            if not check_yn(conflict_val, False):\n                failures.append(f'OPS-5: Conflict=\"{conflict}\" (expected N)')\n            pdf_keywords = ['pdf', 'document', 'file', 'attachment', 'invoice']\n            missing_kw = ['missing', 'no ', 'not ', 'absent', 'lack', 'without', 'unavailable', 'unable', 'could not', \"couldn't\", 'not found', 'pending']\n            support_kw = ['support', 'documentation', 'evidence', 'backup']\n            has_pdf = any(kw in notes for kw in pdf_keywords)\n            has_missing2 = any(kw in notes for kw in missing_kw)\n            has_support = any(kw in notes for kw in support_kw)\n            if not ((has_pdf and has_missing2) or (has_missing2 and has_support) or has_pdf):\n                failures.append(f'OPS-5: Resolution Notes does not indicate missing PDF: \"{notes[:120]}\"')\n            else:\n                details.append(f'OPS-5: OK (SF={sf}, Conflict={conflict}, Notes=\"{notes[:80]}\")')\n\n        # OPS-11\n        if 'OPS-11' not in rows:\n            failures.append('OPS-11: row not found in spreadsheet')\n        else:\n            row = rows['OPS-11']\n            sf_val = safe_get(row, support_found_col)\n            conflict_val = safe_get(row, conflict_col)\n            notes_val = safe_get(row, notes_col)\n            sf = str(sf_val).strip().upper() if sf_val else ''\n            conflict = str(conflict_val).strip().upper() if conflict_val else ''\n            notes = str(notes_val).strip().lower() if notes_val else ''\n            if not check_yn(sf_val, True):\n                failures.append(f'OPS-11: Support Found=\"{sf}\" (expected Y)')\n            if not check_yn(conflict_val, False):\n                failures.append(f'OPS-11: Conflict=\"{conflict}\" (expected N)')\n            manager_kw = ['manager', 'supervisor', 'management']\n            approval_kw = ['approval', 'approved', 'authorize', 'authoriz']\n            finance_kw = ['finance', '#finance', 'finance-approval']\n            missing_kw2 = ['no ', 'not ', 'missing', 'absent', 'without', 'lack', 'pending', 'unable', 'needed']\n            has_manager = any(kw in notes for kw in manager_kw)\n            has_approval2 = any(kw in notes for kw in approval_kw)\n            has_finance = any(kw in notes for kw in finance_kw)\n            has_missing3 = any(kw in notes for kw in missing_kw2)\n            if not ((has_manager or has_approval2) and (has_finance or has_missing3)):\n                failures.append(f'OPS-11: Resolution Notes does not indicate no manager approval in #finance-approvals: \"{notes[:120]}\"')\n            else:\n                details.append(f'OPS-11: OK (SF={sf}, Conflict={conflict}, Notes=\"{notes[:80]}\")')\n\n        # OPS-12\n        if 'OPS-12' not in rows:\n            failures.append('OPS-12: row not found in spreadsheet')\n        else:\n            row = rows['OPS-12']\n            sf_val = safe_get(row, support_found_col)\n            conflict_val = safe_get(row, conflict_col)\n            notes_val = safe_get(row, notes_col)\n            sf = str(sf_val).strip().upper() if sf_val else ''\n            conflict = str(conflict_val).strip().upper() if conflict_val else ''\n            notes = str(notes_val).strip().lower() if notes_val else ''\n            if not check_yn(sf_val, True):\n                failures.append(f'OPS-12: Support Found=\"{sf}\" (expected Y)')\n            if not check_yn(conflict_val, False):\n                failures.append(f'OPS-12: Conflict=\"{conflict}\" (expected N)')\n            manager_kw2 = ['manager', 'supervisor', 'senior', 'management']\n            junior_kw = ['junior', 'analyst', 'jr', 'associate']\n            approval_kw2 = ['approval', 'approved', 'confirm', 'authorize', 'valid', 'invalid', 'self']\n            sop_kw = ['sop', 'policy', 'procedure', 'requirement', 'authority', 'unauthorized', 'insufficient']\n            has_manager2 = any(kw in notes for kw in manager_kw2)\n            has_junior = any(kw in notes for kw in junior_kw)\n            has_approval3 = any(kw in notes for kw in approval_kw2)\n            has_sop = any(kw in notes for kw in sop_kw)\n            negative_kw = ['missing', 'insufficient', 'invalid', 'not a manager', 'not manager', 'escalat', 'exception', 'unauthorized', 'does not', 'cannot', 'should not', 'must be', 'need', 'require', 'lacking', 'improper', 'flag', 'issue', 'concern', 'problem']\n            has_negative = any(kw in notes for kw in negative_kw)\n            if not ((has_junior or has_manager2) and (has_approval3 or has_sop) and (has_negative or has_sop)):\n                failures.append(f'OPS-12: Resolution Notes does not indicate approval must be by manager not junior analyst: \"{notes[:120]}\"')\n            else:\n                details.append(f'OPS-12: OK (SF={sf}, Conflict={conflict}, Notes=\"{notes[:80]}\")')\n\n        if failures:\n            score = max(0.0, 1.0 - (len(failures) / 12.0))\n            return {'pass': False, 'score': score, 'feedback': 'Failures: ' + '; '.join(failures)}\n\n        return {'pass': True, 'score': 1.0, 'feedback': 'All checks passed. ' + '; '.join(details)}\n    except Exception as e:\n        import traceback\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error processing file: {e}\\n{traceback.format_exc()}'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "8c35192a-a420-4647-9f7b-a056ddad0a04",
    "sort_order": 5,
    "rubric_text": "In file `Suspense_Reconciliation_March2026.xlsx`, OPS-6 must show Support Found=Y, Conflict=Y, and Resolution Notes documenting both GL codes (Invoice: 7100-Capital-Equipment and Excel: 8100-IT-Equipment). OPS-10 must show Support Found=Y, Conflict=Y, and Resolution Notes documenting both GL codes (Invoice: 6200-Office-Supplies and Excel: 6200-Office-Exp).",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = wb.active\n        headers = [str(cell.value).strip().lower() if cell.value else '' for cell in ws[1]]\n        id_col = None\n        for i, h in enumerate(headers):\n            if 'id' in h or 'susp' in h or 'transaction' in h or 'item' in h:\n                id_col = i\n                break\n        if id_col is None:\n            id_col = 0\n        support_found_col = None\n        for i, h in enumerate(headers):\n            if 'support found' in h or ('support' in h and 'found' in h):\n                support_found_col = i\n                break\n        if support_found_col is None:\n            for i, h in enumerate(headers):\n                if h.startswith('support'):\n                    support_found_col = i\n                    break\n        conflict_col = None\n        for i, h in enumerate(headers):\n            if 'conflict' in h:\n                conflict_col = i\n                break\n        notes_col = None\n        for i, h in enumerate(headers):\n            if 'resolution' in h or 'notes' in h:\n                notes_col = i\n                break\n        if any(c is None for c in [support_found_col, conflict_col, notes_col]):\n            return {'pass': False, 'score': 0.0, 'feedback': f'Missing required columns. Headers: {headers}'}\n        rows = {}\n        for row in ws.iter_rows(min_row=2, values_only=True):\n            row_id = str(row[id_col]).strip() if row[id_col] else ''\n            if row_id in ('OPS-6', 'OPS-10'):\n                rows[row_id] = row\n        failures = []\n        # OPS-6: Support Found=Y, Conflict=Y, notes with 7100 and 8100\n        if 'OPS-6' not in rows:\n            failures.append('OPS-6: not found')\n        else:\n            row = rows['OPS-6']\n            sf = str(row[support_found_col]).strip().upper() if row[support_found_col] else ''\n            conflict = str(row[conflict_col]).strip().upper() if row[conflict_col] else ''\n            notes = str(row[notes_col]).strip().lower() if row[notes_col] else ''\n            if sf not in ('Y', 'YES', 'TRUE', '1'):\n                failures.append(f'OPS-6: Support Found={sf} (expected Y)')\n            if conflict not in ('Y', 'YES', 'TRUE', '1'):\n                failures.append(f'OPS-6: Conflict={conflict} (expected Y)')\n            has_7100 = '7100' in notes or 'capital' in notes\n            has_8100 = '8100' in notes or 'it' in notes or 'equipment' in notes\n            if not has_7100:\n                failures.append(f'OPS-6: Resolution Notes missing GL code 7100/Capital-Equipment: \"{notes}\"')\n            if not has_8100:\n                failures.append(f'OPS-6: Resolution Notes missing GL code 8100/IT-Equipment: \"{notes}\"')\n        # OPS-10: Support Found=Y, Conflict=Y, notes with 6200-Office-Supplies and 6200-Office-Exp\n        if 'OPS-10' not in rows:\n            failures.append('OPS-10: not found')\n        else:\n            row = rows['OPS-10']\n            sf = str(row[support_found_col]).strip().upper() if row[support_found_col] else ''\n            conflict = str(row[conflict_col]).strip().upper() if row[conflict_col] else ''\n            notes = str(row[notes_col]).strip().lower() if row[notes_col] else ''\n            if sf not in ('Y', 'YES', 'TRUE', '1'):\n                failures.append(f'OPS-10: Support Found={sf} (expected Y)')\n            if conflict not in ('Y', 'YES', 'TRUE', '1'):\n                failures.append(f'OPS-10: Conflict={conflict} (expected Y)')\n            has_6200 = '6200' in notes\n            has_office = 'office' in notes\n            if not has_6200:\n                failures.append(f'OPS-10: Resolution Notes missing GL code 6200: \"{notes}\"')\n            if not has_office:\n                failures.append(f'OPS-10: Resolution Notes missing Office reference: \"{notes}\"')\n        if failures:\n            return {'pass': False, 'score': 0.0, 'feedback': 'Failures: ' + '; '.join(failures)}\n        return {'pass': True, 'score': 1.0, 'feedback': 'OPS-6 and OPS-10 both show Support Found=Y, Conflict=Y, and Resolution Notes documenting both GL codes'}\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error: {e}'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "a5a058cc-1ac7-466b-8799-e7bc4162bba9",
    "sort_order": 6,
    "rubric_text": "In file `Suspense_Reconciliation_March2026.xlsx`, OPS-9 must show Support Found=Y, Conflict=N, Status=EXCEPTION, and Resolution Notes indicating missing Slack approval.",
    "verifier_code": "from pathlib import Path\nimport openpyxl\n\ndef verify(workspace_path, external_services_path=None):\n    xlsx_path = Path(workspace_path) / 'Suspense_Reconciliation_March2026.xlsx'\n    if not xlsx_path.exists():\n        return {'pass': False, 'score': 0.0, 'feedback': 'File Suspense_Reconciliation_March2026.xlsx not found'}\n    try:\n        wb = openpyxl.load_workbook(xlsx_path)\n        ws = wb.active\n        headers = [str(cell.value).strip().lower() if cell.value else '' for cell in ws[1]]\n        id_col = None\n        for i, h in enumerate(headers):\n            if 'id' in h or 'susp' in h or 'transaction' in h or 'item' in h:\n                id_col = i\n                break\n        if id_col is None:\n            id_col = 0\n        status_col = None\n        for i, h in enumerate(headers):\n            if 'status' in h:\n                status_col = i\n                break\n        support_found_col = None\n        for i, h in enumerate(headers):\n            if 'support found' in h or ('support' in h and 'found' in h):\n                support_found_col = i\n                break\n        if support_found_col is None:\n            for i, h in enumerate(headers):\n                if h.startswith('support'):\n                    support_found_col = i\n                    break\n        conflict_col = None\n        for i, h in enumerate(headers):\n            if 'conflict' in h:\n                conflict_col = i\n                break\n        notes_col = None\n        for i, h in enumerate(headers):\n            if 'resolution' in h or 'notes' in h:\n                notes_col = i\n                break\n        if any(c is None for c in [status_col, support_found_col, conflict_col, notes_col]):\n            return {'pass': False, 'score': 0.0, 'feedback': f'Missing required columns. Headers: {headers}'}\n        susp_008_row = None\n        for row in ws.iter_rows(min_row=2, values_only=True):\n            row_id = str(row[id_col]).strip() if row[id_col] else ''\n            if row_id == 'OPS-9':\n                susp_008_row = row\n                break\n        if susp_008_row is None:\n            return {'pass': False, 'score': 0.0, 'feedback': 'OPS-9 not found in workbook'}\n        row = susp_008_row\n        sf = str(row[support_found_col]).strip().upper() if row[support_found_col] else ''\n        conflict = str(row[conflict_col]).strip().upper() if row[conflict_col] else ''\n        status = str(row[status_col]).strip().upper() if row[status_col] else ''\n        notes = str(row[notes_col]).strip().lower() if row[notes_col] else ''\n        failures = []\n        if sf not in ('Y', 'YES', 'TRUE', '1'):\n            failures.append(f'Support Found={sf} (expected Y)')\n        if conflict not in ('N', 'NO', 'FALSE', '0'):\n            failures.append(f'Conflict={conflict} (expected N)')\n        if 'EXCEPTION' not in status:\n            failures.append(f'Status={status} (expected EXCEPTION)')\n        if not any(kw in notes for kw in ['slack', 'approval', 'missing']):\n            failures.append(f'Resolution Notes does not indicate missing Slack approval: \"{notes}\"')\n        if failures:\n            return {'pass': False, 'score': 0.0, 'feedback': 'OPS-9 failures: ' + '; '.join(failures)}\n        return {'pass': True, 'score': 1.0, 'feedback': 'OPS-9 shows Support Found=Y, Conflict=N, Status=EXCEPTION, and Resolution Notes indicating missing Slack approval'}\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error: {e}'}",
    "criterion_type": "expected_output"
  },
  {
    "id": "5f6dad05-f5cf-4e49-9cb1-0f36e9672343",
    "sort_order": 7,
    "rubric_text": "In jira_state.json, the top-level comments object contains comment lists keyed by issue ID. OPS-2, OPS-3, OPS-7, and OPS-8 must each have at least one comment in that list containing the transaction ID, the target GL code, and the phrase \"FIN-100 Cleared & Posted\" (case-insensitive).ve).",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n    # Look for jira_state.json in external_services_path first, then workspace\n    jira_path = None\n    if external_services_path:\n        candidate = Path(external_services_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        candidate = Path(workspace_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'jira_state.json not found in workspace or external_services_path'}\n\n    try:\n        with open(jira_path) as f:\n            jira_data = json.load(f)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error reading jira_state.json: {e}'}\n\n    target_ids = ['OPS-2', 'OPS-3', 'OPS-7', 'OPS-8']\n\n    # The rubric says: \"the top-level comments object contains comment lists keyed by issue ID\"\n    # So we expect jira_data to have a top-level key \"comments\" which is a dict keyed by issue ID\n    # e.g. { \"comments\": { \"OPS-2\": [...], \"OPS-3\": [...], ... }, ... }\n    \n    # Try to find the comments object\n    comments_obj = None\n    if isinstance(jira_data, dict):\n        # Direct top-level \"comments\" key\n        if 'comments' in jira_data:\n            comments_obj = jira_data['comments']\n        # Maybe nested under another key\n        if comments_obj is None:\n            for k, v in jira_data.items():\n                if isinstance(v, dict) and any(tid in v or tid.lower() in str(v).lower() for tid in target_ids):\n                    # Check if this looks like a comments-by-issue-id dict\n                    if any(isinstance(vv, list) for vv in v.values()):\n                        comments_obj = v\n                        break\n    \n    # Fallback: search the entire structure for comment lists\n    if comments_obj is None:\n        # Try to find any dict that has issue IDs as keys mapping to lists\n        def find_comments_dict(obj, depth=0):\n            if depth > 5:\n                return None\n            if isinstance(obj, dict):\n                # Check if this dict itself has target IDs as keys mapping to lists\n                matching = sum(1 for tid in target_ids if tid in obj and isinstance(obj.get(tid), list))\n                if matching >= 1:\n                    return obj\n                for k, v in obj.items():\n                    result = find_comments_dict(v, depth + 1)\n                    if result is not None:\n                        return result\n            return None\n        comments_obj = find_comments_dict(jira_data)\n\n    if comments_obj is None or not isinstance(comments_obj, dict):\n        return {'pass': False, 'score': 0.0, 'feedback': f'No top-level comments object found in jira_state.json. Top-level keys: {list(jira_data.keys()) if isinstance(jira_data, dict) else \"not a dict\"}'}\n\n    failures = []\n    successes = []\n\n    for tid in target_ids:\n        tid_lower = tid.lower()\n\n        # Find the comment list for this issue ID (case-insensitive key match)\n        comment_list = None\n        for key, val in comments_obj.items():\n            if key.lower() == tid_lower or key.upper() == tid.upper():\n                comment_list = val\n                break\n        \n        if comment_list is None:\n            # Also try looking inside issues structure for comments\n            failures.append(f'{tid}: no comment list found keyed by this issue ID in the comments object')\n            continue\n\n        if not isinstance(comment_list, list):\n            comment_list = [comment_list]\n\n        # Check each comment for required content:\n        # 1. transaction ID (the SUSP-XXX)\n        # 2. target GL code (some numeric code or GL reference)\n        # 3. phrase \"FIN-100 Cleared & Posted\" (case-insensitive)\n        found_valid_comment = False\n        best_comment_info = ''\n        for comment in comment_list:\n            comment_text = _get_comment_text(comment)\n            comment_lower = comment_text.lower()\n\n            # Check for transaction ID\n            has_tid = tid_lower in comment_lower\n\n            # Check for \"FIN-100 Cleared & Posted\" (case-insensitive, flexible on & vs and)\n            has_fin100_cleared_posted = bool(\n                re.search(r'fin[\\-\\s]?100\\s+cleared\\s*[&and]+\\s*posted', comment_lower)\n            )\n            # More lenient: just check for fin-100 and cleared and posted separately\n            if not has_fin100_cleared_posted:\n                has_fin100 = bool(re.search(r'fin[\\-\\s]?100', comment_lower))\n                has_cleared = 'cleared' in comment_lower\n                has_posted = 'posted' in comment_lower\n                has_fin100_cleared_posted = has_fin100 and has_cleared and has_posted\n\n            # Check for GL code - look for numeric codes (4+ digits) or GL/account references\n            has_gl = bool(re.search(r'\\d{4,}', comment_text)) or bool(re.search(r'(gl|account|acct|ledger)', comment_lower))\n\n            if has_tid and has_fin100_cleared_posted and has_gl:\n                found_valid_comment = True\n                break\n            \n            # Track what we found for feedback\n            if has_tid or has_fin100_cleared_posted:\n                parts = []\n                if has_tid: parts.append('has TID')\n                if has_fin100_cleared_posted: parts.append('has FIN-100 Cleared & Posted')\n                if has_gl: parts.append('has GL code')\n                best_comment_info = f' (best match: {\", \".join(parts)}, text preview: {comment_text[:200]})'\n\n        if found_valid_comment:\n            successes.append(tid)\n        else:\n            num_comments = len(comment_list)\n            failures.append(f'{tid}: no comment (out of {num_comments}) found containing transaction ID, GL code, and \"FIN-100 Cleared & Posted\"{best_comment_info}')\n\n    score = len(successes) / len(target_ids)\n\n    if failures:\n        return {\n            'pass': False,\n            'score': score,\n            'feedback': f'Passed: {successes}. Failures: ' + '; '.join(failures)\n        }\n\n    return {\n        'pass': True,\n        'score': 1.0,\n        'feedback': f'All cleared items ({\", \".join(target_ids)}) have Jira comments with transaction ID, GL code, and FIN-100 Cleared & Posted'\n    }\n\n\ndef _get_comment_text(comment):\n    \"\"\"Extract text content from a comment object.\"\"\"\n    if isinstance(comment, str):\n        return comment\n    if isinstance(comment, dict):\n        parts = []\n        for key in ('body', 'text', 'content', 'message', 'description', 'note', 'comment'):\n            if key in comment:\n                val = comment[key]\n                if isinstance(val, str):\n                    parts.append(val)\n                elif isinstance(val, list):\n                    parts.append(json.dumps(val))\n                elif isinstance(val, dict):\n                    parts.append(json.dumps(val))\n        if not parts:\n            return json.dumps(comment)\n        return ' '.join(parts)\n    return str(comment)\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "2e4c68b9-6fc5-4ca9-9c09-bc2f3cb0ddf4",
    "sort_order": 8,
    "rubric_text": "In jira_state.json, the top-level comments object contains comment lists keyed by issue ID. OPS-6 must have at least one comment containing both GL codes \"7100\" and \"8100\". OPS-10 must have at least one comment containing both \"6200\" and \"office\". Comments are stored in Atlassian doc format: extract text by walking body → content[] → content[] → text fields.",
    "verifier_code": "from pathlib import Path\nimport json\n\ndef extract_adf_text(node):\n    \"\"\"Recursively extract text from Atlassian Document Format nodes.\"\"\"\n    texts = []\n    if isinstance(node, dict):\n        if 'text' in node:\n            texts.append(str(node['text']))\n        for key in ('content', 'body'):\n            if key in node:\n                val = node[key]\n                if isinstance(val, list):\n                    for item in val:\n                        texts.extend(extract_adf_text(item))\n                elif isinstance(val, dict):\n                    texts.extend(extract_adf_text(val))\n    elif isinstance(node, list):\n        for item in node:\n            texts.extend(extract_adf_text(item))\n    return texts\n\ndef verify(workspace_path, external_services_path=None):\n    # Locate jira_state.json\n    jira_path = None\n    candidates = []\n    if external_services_path:\n        candidates.append(Path(external_services_path) / 'jira_state.json')\n    candidates.append(Path(workspace_path) / 'jira_state.json')\n    for c in candidates:\n        if c.exists():\n            jira_path = c\n            break\n    if jira_path is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'jira_state.json not found in external_services_path or workspace_path'}\n\n    try:\n        with open(jira_path) as f:\n            jira_data = json.load(f)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error reading jira_state.json: {e}'}\n\n    # The rubric says: \"the top-level comments object contains comment lists keyed by issue ID\"\n    # So we look for jira_data[\"comments\"] as a dict keyed by issue ID like \"OPS-6\"\n    comments_obj = None\n    if isinstance(jira_data, dict):\n        # Try direct top-level \"comments\" key\n        if 'comments' in jira_data and isinstance(jira_data['comments'], dict):\n            comments_obj = jira_data['comments']\n\n    # Fallback: search the entire structure for a dict that has keys matching SUSP-xxx\n    if comments_obj is None and isinstance(jira_data, dict):\n        for key, val in jira_data.items():\n            if isinstance(val, dict):\n                # Check if any sub-key looks like an issue ID\n                for subkey in val:\n                    if isinstance(subkey, str) and subkey.upper().startswith('SUSP-'):\n                        comments_obj = val\n                        break\n                if comments_obj:\n                    break\n\n    checks = {\n        'OPS-6': ['7100', '8100'],\n        'OPS-10': ['6200', 'office']\n    }\n    failures = []\n    details = []\n\n    for issue_id, keywords in checks.items():\n        # Try to find comments for this issue\n        comment_list = None\n        if comments_obj is not None:\n            # Try exact key and case-insensitive\n            for k, v in comments_obj.items():\n                if k.upper() == issue_id.upper():\n                    comment_list = v if isinstance(v, list) else [v]\n                    break\n\n        # Fallback: look through issues list for embedded comments\n        if comment_list is None and isinstance(jira_data, dict):\n            issues = []\n            if 'issues' in jira_data and isinstance(jira_data['issues'], list):\n                issues = jira_data['issues']\n            elif 'subtasks' in jira_data and isinstance(jira_data['subtasks'], list):\n                issues = jira_data['subtasks']\n            for issue in issues:\n                if isinstance(issue, dict):\n                    issue_str = json.dumps(issue).lower()\n                    if issue_id.lower() in issue_str:\n                        if 'comments' in issue:\n                            comment_list = issue['comments'] if isinstance(issue['comments'], list) else [issue['comments']]\n                        elif 'comment' in issue:\n                            c = issue['comment']\n                            comment_list = c if isinstance(c, list) else [c]\n                        break\n\n        if comment_list is None or len(comment_list) == 0:\n            failures.append(f'{issue_id}: no comments found for this issue')\n            continue\n\n        # For each comment, extract text using ADF walking and also fallback to full JSON dump\n        found_comment_with_all_keywords = False\n        for comment in comment_list:\n            # Extract text via ADF\n            adf_texts = extract_adf_text(comment)\n            combined_adf = ' '.join(adf_texts).lower()\n\n            # Also get full JSON dump as fallback\n            full_dump = json.dumps(comment).lower()\n\n            # Also check plain string body\n            body_text = ''\n            if isinstance(comment, dict):\n                body_val = comment.get('body', '')\n                if isinstance(body_val, str):\n                    body_text = body_val.lower()\n\n            # Combine all text sources\n            all_text = combined_adf + ' ' + full_dump + ' ' + body_text\n\n            missing = [kw for kw in keywords if kw.lower() not in all_text]\n            if not missing:\n                found_comment_with_all_keywords = True\n                break\n\n        if found_comment_with_all_keywords:\n            details.append(f'{issue_id}: found comment containing all required keywords {keywords}')\n        else:\n            failures.append(f'{issue_id}: no single comment contains all required keywords {keywords}. Checked {len(comment_list)} comment(s).')\n\n    if failures:\n        return {\n            'pass': False,\n            'score': 0.0,\n            'feedback': 'Failures: ' + '; '.join(failures) + ((' | Passes: ' + '; '.join(details)) if details else '')\n        }\n\n    return {\n        'pass': True,\n        'score': 1.0,\n        'feedback': 'All checks passed. ' + '; '.join(details)\n    }\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "b607ef6c-73ef-4e16-8c21-366d6d96d4df",
    "sort_order": 9,
    "rubric_text": "In jira_state.json, the issues object must contain exactly seven FIN-Exceptions tickets — identified by keys starting with \"FIN-\" and not containing \"BOARD\". One ticket must exist for each of: OPS-4 (missing Slack approval), OPS-5 (missing PDF), OPS-6 (GL conflict), OPS-9 (missing Slack approval), OPS-10 (GL conflict), OPS-11 (missing Slack approval), and OPS-12 (self-approval). Each ticket must reference its corresponding SUSP ID somewhere in its content.",
    "verifier_code": "from pathlib import Path\nimport json\n\ndef verify(workspace_path, external_services_path=None):\n    # Locate jira_state.json\n    jira_path = None\n    if external_services_path:\n        candidate = Path(external_services_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        candidate = Path(workspace_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'jira_state.json not found in external_services_path or workspace_path'}\n\n    try:\n        with open(jira_path) as f:\n            jira_data = json.load(f)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error reading jira_state.json: {e}'}\n\n    # The rubric says: \"the issues object must contain exactly seven FIN-Exceptions tickets\n    # identified by keys starting with FIN- and not containing BOARD\"\n    # So we look for jira_data[\"issues\"] as a dict keyed by ticket IDs.\n    issues = None\n    if isinstance(jira_data, dict):\n        # Try to find an \"issues\" key\n        if 'issues' in jira_data:\n            issues = jira_data['issues']\n        else:\n            # Maybe the top-level dict itself is the issues dict\n            issues = jira_data\n\n    if issues is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'Could not find issues object in jira_state.json'}\n\n    # Collect FIN- tickets that don't contain BOARD in their key\n    fin_exception_tickets = {}\n\n    if isinstance(issues, dict):\n        for key, val in issues.items():\n            key_upper = key.upper()\n            if key_upper.startswith('FIN-') and 'BOARD' not in key_upper:\n                fin_exception_tickets[key] = val\n    elif isinstance(issues, list):\n        # If issues is a list, look for key/id fields\n        for item in issues:\n            if isinstance(item, dict):\n                key = str(item.get('key', '') or item.get('id', '') or '')\n                key_upper = key.upper()\n                if key_upper.startswith('FIN-') and 'BOARD' not in key_upper:\n                    fin_exception_tickets[key] = item\n    else:\n        return {'pass': False, 'score': 0.0, 'feedback': f'issues object is of unexpected type: {type(issues)}'}\n\n    # Check count\n    num_tickets = len(fin_exception_tickets)\n    failures = []\n    if num_tickets != 7:\n        failures.append(f'Expected exactly 7 FIN-Exception tickets (keys starting with FIN- and not containing BOARD), found {num_tickets}. Keys found: {list(fin_exception_tickets.keys())}')\n\n    # Required SUSP IDs\n    required_ids = ['OPS-4', 'OPS-5', 'OPS-6', 'OPS-9', 'OPS-10', 'OPS-11', 'OPS-12']\n\n    # For each required SUSP ID, check that at least one FIN-Exception ticket references it\n    found_ids = set()\n    for rid in required_ids:\n        for key, val in fin_exception_tickets.items():\n            # Serialize the entire ticket value to search for the SUSP ID\n            ticket_str = json.dumps(val) if not isinstance(val, str) else val\n            # Also check the key itself\n            full_str = key + ' ' + ticket_str\n            if rid in full_str:\n                found_ids.add(rid)\n                break\n\n    missing_ids = [rid for rid in required_ids if rid not in found_ids]\n    if missing_ids:\n        failures.append(f'Missing FIN-Exception tickets referencing these SUSP IDs: {missing_ids}')\n\n    if failures:\n        score = len(found_ids) / len(required_ids) if not (num_tickets != 7 and len(found_ids) == 0) else 0.0\n        return {'pass': False, 'score': score, 'feedback': 'Failures: ' + '; '.join(failures)}\n\n    return {\n        'pass': True,\n        'score': 1.0,\n        'feedback': f'Found exactly 7 FIN-Exception tickets with keys {list(fin_exception_tickets.keys())}, covering all required SUSP IDs: {required_ids}'\n    }\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "e720abd4-86ab-4a80-80b3-74074d7a2b1e",
    "sort_order": 10,
    "rubric_text": "In jira_state.json, all FIN-Exceptions tickets (for OPS-4, OPS-5, OPS-6, OPS-9, OPS-10, OPS-11, OPS-12) must be assigned to jennifer.walsh.",
    "verifier_code": "from pathlib import Path\nimport json\n\n\ndef _flatten_issues(jira_data):\n    \"\"\"Extract all individual issue dicts from the jira_state structure.\"\"\"\n    all_issues = []\n    if not isinstance(jira_data, dict):\n        if isinstance(jira_data, list):\n            return [i for i in jira_data if isinstance(i, dict)]\n        return []\n\n    for key, val in jira_data.items():\n        if key == 'comments':\n            continue\n        if isinstance(val, list):\n            for item in val:\n                if isinstance(item, dict):\n                    all_issues.append(item)\n        elif isinstance(val, dict):\n            if any(k in val for k in ('key', 'fields', 'id', 'assignee', 'summary')):\n                all_issues.append(val)\n            else:\n                for sub_key, sub_val in val.items():\n                    if isinstance(sub_val, dict):\n                        all_issues.append(sub_val)\n    return all_issues\n\n\ndef _check_assignee_for_jennifer(ticket):\n    \"\"\"Check ONLY the assignee field of a ticket for jennifer.walsh.\"\"\"\n    jennifer_variants = ['jennifer.walsh', 'jennifer walsh', 'jennifer_walsh', 'jwalsh', 'j.walsh']\n\n    if not isinstance(ticket, dict):\n        return False\n\n    containers = [ticket]\n    if isinstance(ticket.get('fields'), dict):\n        containers.append(ticket['fields'])\n\n    for container in containers:\n        for field_name in ['assignee', 'assigned_to', 'owner']:\n            val = container.get(field_name)\n            if val is None:\n                continue\n            if isinstance(val, str):\n                if any(v in val.lower() for v in jennifer_variants):\n                    return True\n            elif isinstance(val, dict):\n                for subfield in ['accountId', 'displayName', 'emailAddress', 'name', 'key']:\n                    subval = val.get(subfield, '')\n                    if subval and isinstance(subval, str):\n                        if any(v in subval.lower() for v in jennifer_variants):\n                            return True\n    return False\n\n\ndef _get_assignee_display(ticket):\n    \"\"\"Extract a human-readable assignee string for error messages.\"\"\"\n    if not isinstance(ticket, dict):\n        return 'unknown'\n    for container in [ticket.get('fields', {}), ticket]:\n        if not isinstance(container, dict):\n            continue\n        for field_name in ['assignee', 'assigned_to', 'owner']:\n            val = container.get(field_name)\n            if val is None:\n                continue\n            if isinstance(val, dict):\n                return val.get('displayName', val.get('accountId', str(val)))\n            if isinstance(val, str) and val:\n                return val\n    return 'unknown'\n\n\ndef _get_issue_key(issue):\n    \"\"\"Return the issue key (e.g. 'FIN-3') if present.\"\"\"\n    if not isinstance(issue, dict):\n        return ''\n    key = issue.get('key')\n    if isinstance(key, str):\n        return key\n    fields = issue.get('fields')\n    if isinstance(fields, dict):\n        k = fields.get('key')\n        if isinstance(k, str):\n            return k\n    return ''\n\n\ndef _get_project_key(issue):\n    \"\"\"Return the project key (e.g. 'FIN') if present.\"\"\"\n    if not isinstance(issue, dict):\n        return ''\n    fields = issue.get('fields') if isinstance(issue.get('fields'), dict) else {}\n    proj = fields.get('project') if isinstance(fields, dict) else None\n    if isinstance(proj, dict):\n        pk = proj.get('key')\n        if isinstance(pk, str):\n            return pk\n    return ''\n\n\ndef _summary_and_description_text(issue):\n    \"\"\"Return summary + description text for matching SUSP-IDs against, lowercased.\"\"\"\n    if not isinstance(issue, dict):\n        return ''\n    fields = issue.get('fields') if isinstance(issue.get('fields'), dict) else issue\n    parts = []\n    summary = fields.get('summary')\n    if isinstance(summary, str):\n        parts.append(summary)\n    desc = fields.get('description')\n    if isinstance(desc, str):\n        parts.append(desc)\n    elif isinstance(desc, dict):\n        # Atlassian Document Format — just dump it; SUSP-XXX will appear as a substring\n        parts.append(json.dumps(desc))\n    return ' '.join(parts)\n\n\ndef verify(workspace_path, external_services_path=None):\n    jira_path = None\n    if external_services_path:\n        candidate = Path(external_services_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        candidate = Path(workspace_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'jira_state.json not found'}\n\n    try:\n        with open(jira_path) as f:\n            jira_data = json.load(f)\n\n        all_issues = _flatten_issues(jira_data)\n\n        # Restrict to FIN-N tickets only (these are the exception tickets the agent creates).\n        # Exclude SUSP-* sub-tasks regardless of their labels.\n        fin_tickets = []\n        for issue in all_issues:\n            ikey = _get_issue_key(issue)\n            pkey = _get_project_key(issue)\n            if ikey.startswith('FIN-') or pkey == 'FIN':\n                # Make sure we don't accidentally include something like 'FIN-Exceptions'\n                # as a key — real keys look like FIN-<number>.\n                tail = ikey.split('-', 1)[1] if '-' in ikey else ''\n                if tail.isdigit() or pkey == 'FIN':\n                    fin_tickets.append(issue)\n\n        required_ids = ['OPS-4', 'OPS-5', 'OPS-6', 'OPS-9',\n                        'OPS-10', 'OPS-11', 'OPS-12']\n        failures = []\n        found_tickets = []\n\n        for rid in required_ids:\n            rid_lower = rid.lower()\n            # Find FIN tickets whose summary/description references this SUSP id\n            matching = [t for t in fin_tickets\n                        if rid_lower in _summary_and_description_text(t).lower()]\n\n            if not matching:\n                failures.append(f'{rid}: no FIN-* exception ticket found referencing this ID')\n                continue\n\n            # If multiple FIN tickets reference the same SUSP id, require that at least one\n            # is assigned to jennifer.walsh. (Adjust to \"all\" if the criterion demands it.)\n            ok_ticket = next((t for t in matching if _check_assignee_for_jennifer(t)), None)\n\n            if ok_ticket is not None:\n                found_tickets.append(rid)\n            else:\n                # Report the first matching ticket's assignee for context\n                t = matching[0]\n                assignee_info = _get_assignee_display(t)\n                tkey = _get_issue_key(t) or '(unknown key)'\n                failures.append(f'{rid}: FIN ticket {tkey} assignee is \"{assignee_info}\", not jennifer.walsh')\n\n        if failures:\n            score = len(found_tickets) / len(required_ids)\n            return {\n                'pass': False,\n                'score': score,\n                'feedback': (f'Found {len(found_tickets)}/{len(required_ids)} correctly assigned. '\n                             f'Failures: ' + '; '.join(failures))\n            }\n\n        return {\n            'pass': True,\n            'score': 1.0,\n            'feedback': (f\"All {len(required_ids)} FIN-Exception tickets \"\n                         f\"({', '.join(required_ids)}) are assigned to jennifer.walsh\")\n        }\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error processing jira_state.json: {e}'}\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "eb714737-6b44-4948-b983-7d1f811133c5",
    "sort_order": 11,
    "rubric_text": "In jira_state.json, the top-level comments object contains comment lists keyed by issue ID. OPS-4, OPS-5, OPS-9, OPS-11, and OPS-12 must each have at least one comment in that list containing an escalation reason. Comments are stored in Atlassian doc format: extract text by walking body → content[] → content[] → text fields. Acceptable keywords indicating an escalation reason include: escalat, exception, missing, conflict, approval, pdf, slack, self, manager.",
    "verifier_code": "from pathlib import Path\nimport json\n\ndef _extract_adf_text(node):\n    \"\"\"Recursively extract text from Atlassian Document Format nodes.\"\"\"\n    texts = []\n    if isinstance(node, dict):\n        if 'text' in node:\n            texts.append(str(node['text']))\n        for key in ('body', 'content', 'children'):\n            child = node.get(key)\n            if isinstance(child, list):\n                for item in child:\n                    texts.extend(_extract_adf_text(item))\n            elif isinstance(child, dict):\n                texts.extend(_extract_adf_text(child))\n    elif isinstance(node, list):\n        for item in node:\n            texts.extend(_extract_adf_text(item))\n    return texts\n\ndef verify(workspace_path, external_services_path=None):\n    # Locate jira_state.json\n    jira_path = None\n    if external_services_path:\n        candidate = Path(external_services_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        candidate = Path(workspace_path) / 'jira_state.json'\n        if candidate.exists():\n            jira_path = candidate\n    if jira_path is None:\n        return {'pass': False, 'score': 0.0, 'feedback': 'jira_state.json not found in external_services_path or workspace_path'}\n\n    try:\n        with open(jira_path) as f:\n            jira_data = json.load(f)\n    except Exception as e:\n        return {'pass': False, 'score': 0.0, 'feedback': f'Error reading jira_state.json: {e}'}\n\n    if not isinstance(jira_data, dict):\n        return {'pass': False, 'score': 0.0, 'feedback': 'jira_state.json top-level is not a dict'}\n\n    # The rubric says: \"the top-level comments object contains comment lists keyed by issue ID\"\n    comments_obj = jira_data.get('comments', {})\n    if not isinstance(comments_obj, dict):\n        # Fallback: maybe the entire jira_data acts as a container; try to find comments elsewhere\n        comments_obj = {}\n\n    # Also collect all issues from other top-level keys (issues list, etc.) for fallback\n    all_issues = []\n    for key, val in jira_data.items():\n        if key == 'comments':\n            continue\n        if isinstance(val, list):\n            all_issues.extend(val)\n        elif isinstance(val, dict):\n            all_issues.append(val)\n\n    required_ids = ['OPS-4', 'OPS-5', 'OPS-9', 'OPS-11', 'OPS-12']\n    escalation_keywords = ['escalat', 'exception', 'missing', 'conflict', 'approval', 'pdf', 'slack', 'self', 'manager']\n\n    failures = []\n    successes = []\n\n    for tid in required_ids:\n        combined_text = ''\n\n        # Primary path: top-level comments object keyed by issue ID\n        comment_list = comments_obj.get(tid, [])\n        if not isinstance(comment_list, list):\n            comment_list = [comment_list] if comment_list else []\n\n        for comment in comment_list:\n            # Extract text via ADF walking\n            adf_texts = _extract_adf_text(comment)\n            combined_text += ' '.join(adf_texts) + ' '\n            # Also grab any plain string fields\n            if isinstance(comment, dict):\n                for field in ('body', 'text', 'content'):\n                    v = comment.get(field)\n                    if isinstance(v, str):\n                        combined_text += v + ' '\n            elif isinstance(comment, str):\n                combined_text += comment + ' '\n\n        # Fallback: look through issues for matching issue and its comments\n        if not combined_text.strip():\n            for issue in all_issues:\n                if not isinstance(issue, dict):\n                    continue\n                issue_str = json.dumps(issue)\n                if tid in issue_str:\n                    # Try to get comments from the issue object\n                    issue_comments = issue.get('comments', issue.get('comment', []))\n                    if not isinstance(issue_comments, list):\n                        issue_comments = [issue_comments] if issue_comments else []\n                    for comment in issue_comments:\n                        adf_texts = _extract_adf_text(comment)\n                        combined_text += ' '.join(adf_texts) + ' '\n                        if isinstance(comment, dict):\n                            for field in ('body', 'text', 'content'):\n                                v = comment.get(field)\n                                if isinstance(v, str):\n                                    combined_text += v + ' '\n                        elif isinstance(comment, str):\n                            combined_text += comment + ' '\n                    # Also check description/notes as last resort\n                    if not combined_text.strip():\n                        desc = str(issue.get('description', '') or '') + ' ' + str(issue.get('notes', '') or '')\n                        combined_text += desc\n                    break\n\n        combined_lower = combined_text.lower().strip()\n\n        if not combined_lower:\n            failures.append(f'{tid}: no comments found in jira_state.json comments object or issue data')\n            continue\n\n        has_escalation = any(kw in combined_lower for kw in escalation_keywords)\n        if has_escalation:\n            matched = [kw for kw in escalation_keywords if kw in combined_lower]\n            # For OPS-12 specifically, require a negative indicator to distinguish\n            # escalation comments (\"approval insufficient\") from clearing comments\n            # (\"approval found\"). This prevents false positives when models clear\n            # OPS-12 instead of excepting it.\n            if tid == 'OPS-12':\n                negative_indicators = ['missing', 'insufficient', 'invalid', 'not a manager', 'not manager', 'escalat', 'exception', 'unauthorized', 'junior', 'self-approv', 'improper', 'flag', 'issue', 'concern', 'problem', 'must be']\n                has_negative = any(neg in combined_lower for neg in negative_indicators)\n                if not has_negative:\n                    failures.append(f'{tid}: comment mentions escalation keywords {matched} but lacks a negative indicator — may be a clearing comment, not an escalation')\n                else:\n                    successes.append(f'{tid}: found escalation keywords {matched} with negative indicator')\n            else:\n                successes.append(f'{tid}: found escalation keywords {matched}')\n        else:\n            failures.append(f'{tid}: comment text found but no escalation keyword detected. Text snippet: \"{combined_lower[:300]}\"')\n\n    score = len(successes) / len(required_ids)\n\n    if failures:\n        feedback = 'Failures: ' + '; '.join(failures)\n        if successes:\n            feedback += ' | Successes: ' + '; '.join(successes)\n        return {'pass': False, 'score': score, 'feedback': feedback}\n\n    return {'pass': True, 'score': 1.0, 'feedback': 'All required subtasks (OPS-4, OPS-5, OPS-9, OPS-11, OPS-12) have comments noting escalation reasons. Details: ' + '; '.join(successes)}\n",
    "criterion_type": "expected_output"
  }
]
