Files
handbook/tasks/logistics_prairie_star_creamery_7b63c60d/tests/rubrics.json
T
2026-06-24 14:16:58 -07:00

24 lines
27 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[
{
"id": "149b0ac0-9db2-47d0-b674-c453f7c2d37d",
"sort_order": 0,
"rubric_text": "The inbound_receiving_log.xlsx file must contain a data row where: the Expected Pallets and Actual Pallets column both contain 24; the Actual Cases column contains 474; the three Temp Reading columns collectively contain 4.5, 6.0 (alternatively 6), and 7.5 in any order; the Variance (Cases) column contains 6; the Lot Codes Received column contains 'A-20260519-01'; the Disposition column contains both 'quarantine' (or 'quarantined') and 'H-Q'; and the Status column contains exactly 'P2 Compound event.'",
"verifier_code": "from pathlib import Path\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n ws = Path(workspace_path)\n\n # Find the receiving log xlsx file\n xlsx_files = list(ws.rglob('*.xlsx'))\n log_file = None\n for f in xlsx_files:\n if 'receiv' in f.name.lower() or 'log' in f.name.lower():\n log_file = f\n break\n if log_file is None and xlsx_files:\n log_file = xlsx_files[0]\n\n if log_file is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'No .xlsx file found in workspace.'}\n\n try:\n import openpyxl\n wb = openpyxl.load_workbook(log_file)\n except Exception as e:\n return {'pass': False, 'score': 0.0, 'feedback': f'Failed to open {log_file.name}: {e}'}\n\n best_result = None\n best_score = -1\n\n for ws_sheet in wb.worksheets:\n rows = list(ws_sheet.iter_rows(values_only=True))\n if len(rows) < 2:\n continue\n\n # Find header row\n header_row_idx = None\n headers = []\n for i, row in enumerate(rows):\n non_none = [c for c in row if c is not None]\n if len(non_none) >= 3:\n header_row_idx = i\n headers = [str(c).strip().lower() if c is not None else '' for c in row]\n break\n\n if header_row_idx is None:\n continue\n\n def find_col(patterns):\n results = []\n for idx, h in enumerate(headers):\n for p in patterns:\n if p in h:\n results.append(idx)\n break\n return results\n\n expected_pallet_cols = find_col(['expected pallet'])\n actual_pallet_cols = find_col(['actual pallet'])\n pallet_cols = find_col(['pallet'])\n actual_case_cols = find_col(['actual case'])\n case_cols = find_col(['case'])\n temp_cols = find_col(['temp'])\n variance_cols = find_col(['variance'])\n lot_cols = find_col(['lot'])\n disposition_cols = find_col(['disposition'])\n status_cols = find_col(['status'])\n\n data_rows = rows[header_row_idx + 1:]\n for row in data_rows:\n row_str_list = [str(c).strip() if c is not None else '' for c in row]\n row_lower = ' '.join(row_str_list).lower()\n\n non_empty = [c for c in row if c is not None and str(c).strip() != '']\n if len(non_empty) < 3:\n continue\n\n def get_vals(col_indices):\n vals = []\n for ci in col_indices:\n if ci < len(row) and row[ci] is not None:\n vals.append(row[ci])\n return vals\n\n def num_eq(val, target, tol=0.5):\n try:\n return abs(float(val) - target) < tol\n except (ValueError, TypeError):\n s = str(val).replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-')\n m = re.search(r'-?\\d+\\.?\\d*', s)\n if m:\n try:\n return abs(float(m.group()) - target) < tol\n except:\n pass\n return False\n\n # Check Expected Pallets = 24\n expected_pallet_ok = False\n for v in get_vals(expected_pallet_cols):\n if num_eq(v, 24, 0.5):\n expected_pallet_ok = True\n if not expected_pallet_ok:\n for v in get_vals(pallet_cols):\n if num_eq(v, 24, 0.5):\n expected_pallet_ok = True\n break\n if not expected_pallet_ok:\n for v in row:\n if v is not None and num_eq(v, 24, 0.5):\n expected_pallet_ok = True\n break\n\n # Check Actual Pallets = 24\n actual_pallet_ok = False\n for v in get_vals(actual_pallet_cols):\n if num_eq(v, 24, 0.5):\n actual_pallet_ok = True\n if not actual_pallet_ok:\n for v in get_vals(pallet_cols):\n if num_eq(v, 24, 0.5):\n actual_pallet_ok = True\n break\n if not actual_pallet_ok:\n for v in row:\n if v is not None and num_eq(v, 24, 0.5):\n actual_pallet_ok = True\n break\n\n pallets_ok = expected_pallet_ok and actual_pallet_ok\n\n # Check Actual Cases = 474\n cases_ok = False\n for v in get_vals(actual_case_cols):\n if num_eq(v, 474, 0.5):\n cases_ok = True\n if not cases_ok:\n for v in get_vals(case_cols):\n if num_eq(v, 474, 0.5):\n cases_ok = True\n break\n if not cases_ok:\n for v in row:\n if v is not None and num_eq(v, 474, 0.5):\n cases_ok = True\n break\n\n # Check temps: need -4.5, -6.0 (or -6), -7.5\n temp_vals_raw = get_vals(temp_cols)\n if not temp_vals_raw:\n temp_vals_raw = [v for v in row if v is not None]\n\n temp_nums = []\n for v in temp_vals_raw:\n try:\n temp_nums.append(float(v))\n except (ValueError, TypeError):\n s = str(v).replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-')\n matches = re.findall(r'-?\\d+\\.?\\d*', s)\n for m in matches:\n try:\n temp_nums.append(float(m))\n except:\n pass\n\n has_neg4_5 = any(abs(t - (-4.5)) < 0.15 for t in temp_nums)\n has_neg6 = any(abs(t - (-6.0)) < 0.15 for t in temp_nums)\n has_neg7_5 = any(abs(t - (-7.5)) < 0.15 for t in temp_nums)\n temps_ok = has_neg4_5 and has_neg6 and has_neg7_5\n\n # Check variance = -6\n variance_ok = False\n for v in get_vals(variance_cols):\n if num_eq(v, -6, 0.5):\n variance_ok = True\n if not variance_ok:\n for v in get_vals(variance_cols):\n s = str(v).replace('\\u2212', '-').replace('\\u2013', '-')\n if re.search(r'-\\s*6(\\.0)?\\s*$', s.strip()):\n variance_ok = True\n\n # Check lot code A-20260519-01\n lot_ok = False\n for v in get_vals(lot_cols):\n if 'a-20260519-01' in str(v).lower():\n lot_ok = True\n if not lot_ok:\n if 'a-20260519-01' in row_lower:\n lot_ok = True\n\n # Check disposition: quarantine/quarantined AND H-Q\n disp_vals = get_vals(disposition_cols)\n disp_text = ' '.join(str(v) for v in disp_vals).lower() if disp_vals else row_lower\n disp_text_norm = disp_text.replace('\\u2212', '-').replace('\\u2013', '-')\n quarantine_ok = bool(re.search(r'quarantin', disp_text_norm))\n hq_ok = bool(re.search(r'h[\\-\\s]?q', disp_text_norm))\n\n # Check status: P2 - Compound event (with flexible dash)\n status_vals = get_vals(status_cols)\n status_text = ' '.join(str(v) for v in status_vals).lower() if status_vals else row_lower\n status_text_norm = status_text.replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-')\n has_p2 = bool(re.search(r'p\\s*2', status_text_norm))\n has_compound = bool(re.search(r'compound\\s*event', status_text_norm))\n status_ok = has_p2 and has_compound\n\n checks = [\n ('Expected Pallets = 24', expected_pallet_ok),\n ('Actual Pallets = 24', actual_pallet_ok),\n ('Actual Cases = 474', cases_ok),\n ('Temp -4.5', has_neg4_5),\n ('Temp -6.0/-6', has_neg6),\n ('Temp -7.5', has_neg7_5),\n ('Variance (Cases) = -6', variance_ok),\n ('Lot Code A-20260519-01', lot_ok),\n ('Disposition: quarantine', quarantine_ok),\n ('Disposition: H-Q', hq_ok),\n ('Status: P2', has_p2),\n ('Status: Compound event', has_compound),\n ]\n\n passed_checks = [name for name, ok in checks if ok]\n failed_checks = [name for name, ok in checks if not ok]\n score = len(passed_checks) / len(checks)\n\n if score > best_score:\n best_score = score\n if not failed_checks:\n best_result = {\n 'pass': True,\n 'score': 1.0,\n 'feedback': f'All required fields found in {log_file.name}: {\", \".join(passed_checks)}.'\n }\n else:\n best_result = {\n 'pass': False,\n 'score': score,\n 'feedback': f'In {log_file.name}, found: {\", \".join(passed_checks)}. Missing: {\", \".join(failed_checks)}. Row values: {row_str_list}'\n }\n\n if best_result is not None and best_score >= 0.4:\n return best_result\n\n # Fallback: read all text from all files and do loose matching\n candidates = set()\n for pattern in ['*.txt', '*.md', '*.csv', '*.json', '*.log']:\n candidates.update(ws.glob(pattern))\n candidates.update(ws.rglob('*receiv*'))\n candidates.update(ws.rglob('*log*'))\n candidates.update(ws.rglob('*.xlsx'))\n candidates.update(ws.rglob('*.csv'))\n candidates.update(ws.rglob('*.txt'))\n candidates.update(ws.rglob('*.md'))\n\n all_text = ''\n files_read = []\n\n for f in candidates:\n try:\n if f.is_dir():\n continue\n if f.suffix.lower() == '.xlsx':\n import openpyxl as oxl\n wb2 = oxl.load_workbook(f)\n for s in wb2.worksheets:\n for row in s.iter_rows(values_only=True):\n for cell in row:\n if cell is not None:\n all_text += str(cell) + ' '\n files_read.append(str(f.name))\n elif f.suffix.lower() == '.docx':\n from docx import Document\n doc = Document(f)\n content = ' '.join([p.text for p in doc.paragraphs])\n all_text += content + ' '\n files_read.append(str(f.name))\n else:\n content = f.read_text(encoding='utf-8', errors='ignore')\n all_text += content + ' '\n files_read.append(str(f.name))\n except Exception:\n continue\n\n if not all_text.strip():\n return {'pass': False, 'score': 0.0, 'feedback': 'No readable content found in workspace.'}\n\n normalized = all_text.replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-').replace('\\u00b0', ' ')\n text_lower = normalized.lower()\n\n checks = []\n\n # 1. pallets 24\n checks.append(('Pallets 24', '24' in text_lower))\n\n # 2. cases 474\n checks.append(('Actual Cases 474', '474' in text_lower))\n\n # 3. temps\n checks.append(('Temp -4.5', bool(re.search(r'-\\s*4\\.5', normalized))))\n checks.append(('Temp -6.0/-6', bool(re.search(r'-\\s*6\\.0', normalized) or re.search(r'-\\s*6(?![\\d.])', normalized))))\n checks.append(('Temp -7.5', bool(re.search(r'-\\s*7\\.5', normalized))))\n\n # 4. variance -6\n checks.append(('Variance -6', bool(re.search(r'-\\s*6', normalized))))\n\n # 5. lot code\n checks.append(('Lot Code A-20260519-01', 'a-20260519-01' in text_lower))\n\n # 6. quarantine\n checks.append(('Disposition: quarantine', bool(re.search(r'quarantin', text_lower))))\n\n # 7. H-Q\n checks.append(('Disposition: H-Q', bool(re.search(r'h[\\-\\s]?q', text_lower))))\n\n # 8. P2\n checks.append(('Status: P2', bool(re.search(r'p[\\-\\s]?2', text_lower))))\n\n # 9. Compound event\n checks.append(('Status: Compound event', bool(re.search(r'compound\\s*event', text_lower))))\n\n passed_checks = [name for name, ok in checks if ok]\n failed_checks = [name for name, ok in checks if not ok]\n score = len(passed_checks) / len(checks) if checks else 0.0\n\n if not failed_checks:\n return {'pass': True, 'score': 1.0, 'feedback': f'All required fields found (fallback text search): {\", \".join(passed_checks)}. Files: {\", \".join(files_read[:10])}'}\n else:\n return {'pass': False, 'score': score, 'feedback': f'Missing: {\", \".join(failed_checks)}. Found: {\", \".join(passed_checks)}. Files: {\", \".join(files_read[:10])}'}\n",
"criterion_type": "expected_output"
},
{
"id": "a20b335b-2dbd-4e0c-8f52-1500a16fd68f",
"sort_order": 1,
"rubric_text": "A message posted to the #dc-escalations Slack channel must begin with 'P2 escalation. Compound.' [exact string, 'compound event' not acceptable], contain manifest number '2026-05-20-001', contain the exact phrase 'Escalated to Director of Supply Chain.', and contain the exact phrase 'Incident report: awaiting Director decision.' The Detail and Action required fields may contain any text.",
"verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n if external_services_path is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'external_services_path is None; cannot check slack_data.json'}\n\n slack_path = Path(external_services_path) / 'slack_data.json'\n if not slack_path.exists():\n return {'pass': False, 'score': 0.0, 'feedback': 'slack_data.json not found at external_services_path'}\n\n with open(slack_path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n\n # Find the #dc-escalations channel\n channels = data.get('channels', {})\n escalation_channel_id = None\n for cid, cinfo in channels.items():\n cname = str(cinfo.get('name', '')).lower().strip()\n if cname == 'dc-escalations':\n escalation_channel_id = cid\n break\n\n if escalation_channel_id is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'Could not find #dc-escalations channel in slack_data.json. Channels found: ' + str([c.get('name', '') for c in channels.values()])}\n\n # Get messages in that channel\n messages_by_channel = data.get('messages', {})\n channel_messages = messages_by_channel.get(escalation_channel_id, [])\n\n if not channel_messages:\n return {'pass': False, 'score': 0.0, 'feedback': f'No messages found in #dc-escalations channel (channel ID: {escalation_channel_id})'}\n\n # Score each message to find the best candidate\n best_msg = None\n best_score = -1\n\n for msg in channel_messages:\n text = str(msg.get('text', ''))\n text_lower = text.lower()\n score = 0\n if text_lower.lstrip().startswith('p2 escalation'):\n score += 5\n if '2026-05-20-001' in text:\n score += 5\n if 'compound' in text_lower:\n score += 3\n if 'director of supply chain' in text_lower:\n score += 3\n if 'awaiting director decision' in text_lower:\n score += 3\n if score > best_score:\n best_score = score\n best_msg = msg\n\n if best_msg is None or best_score <= 0:\n texts_preview = [str(m.get('text', ''))[:80] for m in channel_messages[:5]]\n return {'pass': False, 'score': 0.0, 'feedback': f'No plausible escalation message found in #dc-escalations among {len(channel_messages)} messages. Previews: {texts_preview}'}\n\n text = str(best_msg.get('text', ''))\n text_lower = text.lower()\n\n failures = []\n details = []\n\n # (1) Must begin with 'P2 escalation. Compound.' [exact string, 'compound event' not acceptable]\n # The rubric says exact string 'P2 escalation. Compound.' at the start.\n # We check case-insensitively for leniency, and allow minor whitespace variations,\n # but 'compound' must appear as a standalone word (not 'compound event' replacing it).\n # The key distinction: must say 'Compound' (with period/punctuation after), NOT 'Compound event'.\n start_pattern = re.compile(r'(?i)^\\s*p2\\s+escalation\\.\\s*compound\\s*\\.', re.DOTALL)\n if start_pattern.search(text):\n # Additionally verify it doesn't say 'compound event' right at the start\n bad_pattern = re.compile(r'(?i)^\\s*p2\\s+escalation\\.\\s*compound\\s+event', re.DOTALL)\n if bad_pattern.search(text):\n failures.append(f\"(1) Message begins with 'P2 escalation. Compound event...' but rubric requires exactly 'P2 escalation. Compound.' (not 'compound event'). Start: '{text[:80]}...'\")\n else:\n details.append(\"(1) Message begins with 'P2 escalation. Compound.' \\u2713\")\n else:\n failures.append(f\"(1) Message must begin with 'P2 escalation. Compound.' but starts with: '{text[:80]}...'\")\n\n # (2) Must contain manifest number '2026-05-20-001'\n if '2026-05-20-001' in text:\n details.append(\"(2) Manifest number '2026-05-20-001' found \\u2713\")\n else:\n failures.append(\"(2) Manifest number '2026-05-20-001' not found in message\")\n\n # (3) Must contain the exact phrase 'Escalated to Director of Supply Chain.'\n # Be loose: case-insensitive, allow with or without trailing period\n if re.search(r'(?i)escalated\\s+to\\s+director\\s+of\\s+supply\\s+chain\\.?', text):\n details.append(\"(3) 'Escalated to Director of Supply Chain.' found \\u2713\")\n else:\n failures.append(\"(3) Exact phrase 'Escalated to Director of Supply Chain.' not found in message\")\n\n # (4) Must contain the exact phrase 'Incident report: awaiting Director decision.'\n # Be loose: case-insensitive, allow with or without trailing period, flexible colon/space\n if re.search(r'(?i)incident\\s+report\\s*[:\\-]\\s*awaiting\\s+director\\s+decision\\.?', text):\n details.append(\"(4) 'Incident report: awaiting Director decision.' found \\u2713\")\n else:\n failures.append(\"(4) Exact phrase 'Incident report: awaiting Director decision.' not found in message\")\n\n if failures:\n feedback = 'Escalation message verification failures: ' + '; '.join(failures)\n if details:\n feedback += ' | Passed: ' + '; '.join(details)\n feedback += f' | Message preview: {text[:400]}'\n return {'pass': False, 'score': 0.0, 'feedback': feedback}\n\n return {'pass': True, 'score': 1.0, 'feedback': 'All #dc-escalations message criteria satisfied. ' + '; '.join(details)}\n",
"criterion_type": "expected_output"
},
{
"id": "98ee41bc-57a2-4c54-a5c1-57bf70a8aee9",
"sort_order": 2,
"rubric_text": "A sent email in mailbox.json must be addressed To: daniel.reyes@prairiestar.com only, CC: carlos.mendoza@prairiestar.com and jacob.patel@prairiestar.com with no additional recipients, have a subject containing 'P2', 'Compound Event', '2026-05-20-001', and '2026-05-20', and have a body containing all three temperature readings 4.5, 6.0 (alternatively 6), and 7.5, 'Tier 3', variance of 6 cases and 1.25%, 'Moderate' as the variance classification, 'quarantined to H-Q', and 'Director assessment required' [exact string].",
"verifier_code": "from pathlib import Path\nimport json\nimport re\n\ndef verify(workspace_path, external_services_path=None):\n if external_services_path is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'external_services_path is None; cannot check mailbox.json'}\n\n mailbox_path = Path(external_services_path) / 'mailbox.json'\n if not mailbox_path.exists():\n return {'pass': False, 'score': 0.0, 'feedback': 'mailbox.json not found'}\n\n with open(mailbox_path, 'r', encoding='utf-8') as f:\n data = json.load(f)\n\n sent_emails = [e for e in data.get('emails', []) if e.get('folder', '').lower() == 'sent']\n\n if not sent_emails:\n return {'pass': False, 'score': 0.0, 'feedback': 'No sent emails found in mailbox.json'}\n\n # Find the escalation email - look for one addressed to daniel.reyes\n matching_email = None\n for email in sent_emails:\n to_addr = (email.get('to_addr') or email.get('to') or '').lower()\n if 'daniel.reyes@prairiestar.com' in to_addr:\n matching_email = email\n break\n\n if matching_email is None:\n # Fallback: look for email with P2 in subject or relevant content\n for email in sent_emails:\n subject = (email.get('subject') or '').lower()\n body = (email.get('body_text') or email.get('body') or '').lower()\n if 'p2' in subject or ('quarantine' in body and 'h-q' in body):\n matching_email = email\n break\n\n if matching_email is None:\n return {'pass': False, 'score': 0.0, 'feedback': 'No escalation email found in sent folder addressed to daniel.reyes@prairiestar.com'}\n\n failures = []\n\n # --- Check TO field ---\n to_addr = (matching_email.get('to_addr') or matching_email.get('to') or '').lower().strip()\n if 'daniel.reyes@prairiestar.com' not in to_addr:\n failures.append('To field does not contain daniel.reyes@prairiestar.com')\n else:\n to_emails = re.findall(r'[\\w.+-]+@[\\w.-]+', to_addr)\n to_emails_filtered = [e for e in to_emails if e != 'daniel.reyes@prairiestar.com']\n if to_emails_filtered:\n failures.append(f'To field contains additional recipients beyond daniel.reyes@prairiestar.com: {to_emails_filtered}')\n\n # --- Check CC field ---\n cc_addr = (matching_email.get('cc_addr') or matching_email.get('cc') or '').lower().strip()\n cc_emails = re.findall(r'[\\w.+-]+@[\\w.-]+', cc_addr)\n required_cc = {'carlos.mendoza@prairiestar.com', 'jacob.patel@prairiestar.com'}\n cc_set = set(cc_emails)\n missing_cc = required_cc - cc_set\n extra_cc = cc_set - required_cc\n if missing_cc:\n failures.append(f'CC field missing required recipients: {missing_cc}')\n if extra_cc:\n failures.append(f'CC field contains extra recipients: {extra_cc}')\n\n # --- Check Subject ---\n subject = matching_email.get('subject') or ''\n subject_lower = subject.lower()\n if 'p2' not in subject_lower:\n failures.append('Subject does not contain P2')\n if 'compound event' not in subject_lower:\n failures.append('Subject does not contain Compound Event')\n if '2026-05-20-001' not in subject:\n failures.append('Subject does not contain 2026-05-20-001')\n if '2026-05-20' not in subject:\n failures.append('Subject does not contain 2026-05-20')\n\n # --- Check Body ---\n body = matching_email.get('body_text') or matching_email.get('body') or ''\n # Normalize unicode minus signs and en-dashes to regular minus/hyphen\n normalized_body = body.replace('\\u2212', '-').replace('\\u2013', '-').replace('\\u2014', '-').replace('\\u00b0', ' ')\n normalized_lower = normalized_body.lower()\n\n # Temperature readings: -4.5, -6.0 (or -6), -7.5\n if not re.search(r'[-]\\s*4\\.5', normalized_lower):\n failures.append('Body does not contain temperature reading -4.5 (or \\u22124.5)')\n\n # -6.0 or -6 (but distinguish from -6 in other contexts like variance)\n has_minus6 = bool(re.search(r'[-]\\s*6\\.0', normalized_lower)) or bool(re.search(r'[-]\\s*6(?:\\b|\\s|f|$|,|\\)|\\*|\\xb0)', normalized_lower))\n if not has_minus6:\n failures.append('Body does not contain temperature reading -6.0 or -6 (or \\u22126.0/\\u22126)')\n\n if not re.search(r'[-]\\s*7\\.5', normalized_lower):\n failures.append('Body does not contain temperature reading -7.5 (or \\u22127.5)')\n\n # Tier 3\n if not re.search(r'tier[\\s\\-_]*3', normalized_lower):\n failures.append('Body does not contain Tier 3')\n\n # Variance: -6 cases and 1.25%\n has_variance_cases = bool(re.search(r'[-]\\s*6\\s*case', normalized_lower)) or \\\n bool(re.search(r'varianc.{0,80}[-]\\s*6', normalized_lower)) or \\\n bool(re.search(r'[-]\\s*6.{0,30}case', normalized_lower)) or \\\n bool(re.search(r'shortage.{0,30}6', normalized_lower)) or \\\n bool(re.search(r'short.{0,30}6\\s*case', normalized_lower)) or \\\n bool(re.search(r'6\\s*case.{0,30}short', normalized_lower))\n if not has_variance_cases:\n failures.append('Body does not state variance of -6 cases')\n\n if '1.25' not in normalized_body:\n failures.append('Body does not state variance percentage 1.25%')\n else:\n # Check if 1.25 is near a percent sign or the word percent\n if not re.search(r'1\\.25\\s*(%|percent)', normalized_lower):\n failures.append('Body contains 1.25 but not clearly as 1.25%')\n\n # 'Moderate'\n if 'moderate' not in normalized_lower:\n failures.append('Body does not contain Moderate')\n\n # 'quarantined to H-Q' - check that quarantined and H-Q are present and near each other\n if 'h-q' not in normalized_lower and 'h\\u2011q' not in normalized_lower:\n failures.append('Body does not contain H-Q')\n if 'quarantine' not in normalized_lower and 'quarantined' not in normalized_lower:\n failures.append('Body does not contain quarantine or quarantined')\n\n # 'Director assessment required' - exact string check (case-insensitive)\n if not re.search(r'director\\s+assessment\\s+required', normalized_lower):\n failures.append('Body does not contain exact string \"Director assessment required\"')\n\n if failures:\n return {'pass': False, 'score': 0.0, 'feedback': 'Email issues: ' + '; '.join(failures)}\n\n return {'pass': True, 'score': 1.0, 'feedback': 'Email correctly addressed To: daniel.reyes@prairiestar.com, CC: carlos.mendoza and jacob.patel, subject contains P2/Compound Event/2026-05-20-001/2026-05-20, body contains all required content: temps -4.5/-6.0/-7.5, Tier 3, variance -6 cases/1.25%, Moderate, quarantined to H-Q, Director assessment required.'}\n",
"criterion_type": "expected_output"
}
]