[
  {
    "id": "fd5fea82-8bfe-48e1-81d0-be8b87856e35",
    "sort_order": 0,
    "rubric_text": "In `jira_state.json`, CLM_LIFE-1 must NOT have CLM-910 (Identity Verification Hold) or CLM-920 (Complex Beneficiary Hold) set.",
    "verifier_code": "from pathlib import Path\nimport json\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    # --- Locate CLM_LIFE-1 ---\n    issue = None\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n\n    if isinstance(issues, dict):\n        issue = issues.get(\"CLM_LIFE-1\")\n        if issue is None:\n            for key, val in issues.items():\n                if key.upper() == \"CLM_LIFE-1\":\n                    issue = val\n                    break\n\n    if issue is None and isinstance(data, list):\n        for item in data:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == \"CLM_LIFE-1\":\n                issue = item\n                break\n\n    if issue is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-1 not found in jira_state.json\"}\n\n    # --- Extract status name (handles nested dict or plain string) ---\n    def get_status_name(issue):\n        fields = issue.get(\"fields\", {})\n        if isinstance(fields, dict):\n            status_field = fields.get(\"status\")\n            if isinstance(status_field, dict):\n                return str(status_field.get(\"name\", \"\")).strip()\n            elif isinstance(status_field, str):\n                return status_field.strip()\n        top_status = issue.get(\"status\")\n        if isinstance(top_status, dict):\n            return str(top_status.get(\"name\", \"\")).strip()\n        elif isinstance(top_status, str):\n            return top_status.strip()\n        return \"\"\n\n    status_name = get_status_name(issue)\n    status_lower = status_name.lower()\n\n    # --- Extract labels ---\n    labels = []\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        raw_labels = fields.get(\"labels\", [])\n        if isinstance(raw_labels, list):\n            labels = [str(l).lower() for l in raw_labels]\n\n    # --- Check CLM-910 NOT set (status and labels only, NOT comment text) ---\n    has_910 = (\n        \"clm-910\" in status_lower\n        or \"identity verification hold\" in status_lower\n        or any(\"clm-910\" in lbl for lbl in labels)\n        or any(\"identity-verification-hold\" in lbl or \"identity verification hold\" in lbl for lbl in labels)\n    )\n\n    # --- Check CLM-920 NOT set (status and labels only) ---\n    has_920 = (\n        \"clm-920\" in status_lower\n        or \"complex beneficiary hold\" in status_lower\n        or any(\"clm-920\" in lbl for lbl in labels)\n        or any(\"complex-beneficiary-hold\" in lbl or \"complex beneficiary hold\" in lbl for lbl in labels)\n    )\n\n    # --- Check status is \"Route to AP for Payout\" ---\n    routed = \"route to ap for payout\" in status_lower\n\n    if not routed:\n        transitions = issue.get(\"transitions\", [])\n        if isinstance(transitions, list):\n            for t in transitions:\n                t_name = \"\"\n                if isinstance(t, dict):\n                    t_name = str(t.get(\"name\", t.get(\"to\", \"\"))).lower()\n                elif isinstance(t, str):\n                    t_name = t.lower()\n                if \"route to ap for payout\" in t_name:\n                    routed = True\n                    break\n\n    # --- Build result ---\n    issues_found = []\n    if has_910:\n        issues_found.append(f\"CLM-910 Identity Verification Hold is set (status: '{status_name}')\")\n    if has_920:\n        issues_found.append(f\"CLM-920 Complex Beneficiary Hold is set (status: '{status_name}')\")\n    if issues_found:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-1: \" + \"; \".join(issues_found)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"CLM_LIFE-1: No CLM-910, no CLM-920\",\n    }",
    "criterion_type": "incorrect_behavior"
  },
  {
    "id": "3328f333-48c7-45b0-a940-a7e9e52f2b5f",
    "sort_order": 1,
    "rubric_text": "In `jira_state.json`, CLM_LIFE-1 must have a structured beneficiary allocation comment containing Policy LC-2019-08844, Face Amount $400,000.00, Maria L. Martinez (Primary, 60%, $240,000.00), David R. Martinez (Primary, 40%, $160,000.00), with a header line and Analyst/Date attribution (\"analyst\" and the date must appear within 80 characters of each other), and must NOT include Elena Martinez (mention of her exclusion is acceptable).",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\n\ndef extract_text_from_adf(node):\n    \"\"\"Recursively extract plain text from Atlassian Document Format.\"\"\"\n    text = \"\"\n    if isinstance(node, dict):\n        if node.get(\"type\") == \"text\":\n            text += node.get(\"text\", \"\")\n        for child in node.get(\"content\", []):\n            text += extract_text_from_adf(child)\n    elif isinstance(node, list):\n        for item in node:\n            text += extract_text_from_adf(item)\n    return text\n\n\ndef get_all_comment_text(data, issue_key):\n    \"\"\"Get all comment text for an issue from both top-level and inline locations.\"\"\"\n    text = \"\"\n    # 1. Top-level comments dict\n    for comment in data.get(\"comments\", {}).get(issue_key, []):\n        body = comment.get(\"body\", {})\n        text += extract_text_from_adf(body) + \"\\n\"\n    # 2. Inline comments on the issue (fields.comment.comments)\n    issues = data.get(\"issues\", {})\n    issue = issues.get(issue_key, {}) if isinstance(issues, dict) else {}\n    fields = issue.get(\"fields\", {}) if isinstance(issue, dict) else {}\n    if isinstance(fields, dict):\n        comment_field = fields.get(\"comment\", {})\n        if isinstance(comment_field, dict):\n            for c in comment_field.get(\"comments\", []):\n                body = c.get(\"body\", {})\n                text += extract_text_from_adf(body) + \"\\n\"\n    return text\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    # --- Locate CLM_LIFE-1 ---\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    issue = None\n    if isinstance(issues, dict):\n        issue = issues.get(\"CLM_LIFE-1\")\n        if issue is None:\n            for key, val in issues.items():\n                if key.upper() == \"CLM_LIFE-1\":\n                    issue = val\n                    break\n    if issue is None and isinstance(data, list):\n        for item in data:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == \"CLM_LIFE-1\":\n                issue = item\n                break\n\n    if issue is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-1 not found in jira_state.json\"}\n\n    # --- Get all comment text (ADF-aware, checks both top-level and inline) ---\n    comments_text = get_all_comment_text(data, \"CLM_LIFE-1\")\n    ct_lower = comments_text.lower()\n\n    failures = []\n\n    # Check header line: \"beneficiary allocation\" somewhere in the comment\n    if \"beneficiary allocation\" not in ct_lower:\n        failures.append(\"Missing 'Beneficiary Allocation' header line\")\n\n    # Check policy LC-2019-08844\n    if \"lc-2019-08844\" not in ct_lower:\n        failures.append(\"Missing Policy LC-2019-08844\")\n\n    # Check face amount $400,000.00\n    if \"400,000\" not in comments_text and \"400000\" not in comments_text:\n        failures.append(\"Missing Face Amount $400,000.00\")\n\n    # Check Maria L. Martinez (Primary, 60%, $240,000.00)\n    if \"maria\" not in ct_lower or \"martinez\" not in ct_lower:\n        failures.append(\"Missing beneficiary Maria L. Martinez\")\n    else:\n        if \"240,000\" not in comments_text and \"240000\" not in comments_text:\n            failures.append(\"Missing Maria Martinez amount $240,000.00\")\n        if \"60%\" not in comments_text and \"60 %\" not in comments_text:\n            failures.append(\"Missing Maria Martinez 60% split\")\n\n    # Check David R. Martinez (Primary, 40%, $160,000.00)\n    if \"david\" not in ct_lower:\n        failures.append(\"Missing beneficiary David R. Martinez\")\n    else:\n        if \"160,000\" not in comments_text and \"160000\" not in comments_text:\n            failures.append(\"Missing David Martinez amount $160,000.00\")\n        if \"40%\" not in comments_text and \"40 %\" not in comments_text:\n            failures.append(\"Missing David Martinez 40% split\")\n\n    # Check Elena Martinez NOT included as a beneficiary in the allocation\n    # Mention of her exclusion (e.g., \"Elena Martinez excluded\" or \"not included\") is acceptable.\n    # We look for 'elena' in the allocation section and check if it's in the context of exclusion.\n    alloc_start = ct_lower.find(\"beneficiary allocation\")\n    if alloc_start != -1:\n        alloc_section = ct_lower[alloc_start:]\n        if \"elena\" in alloc_section:\n            # Check if Elena is mentioned only in context of exclusion\n            # Look for exclusion-related keywords near 'elena'\n            elena_idx = alloc_section.find(\"elena\")\n            surrounding = alloc_section[max(0, elena_idx - 80):elena_idx + 80]\n            exclusion_keywords = [\"exclud\", \"not included\", \"contingent\", \"not eligible\", \"removed\", \"n/a\", \"omit\", \"not applicable\", \"not payable\", \"skipped\", \"does not\"]\n            is_exclusion_mention = any(kw in surrounding for kw in exclusion_keywords)\n            if not is_exclusion_mention:\n                failures.append(\"Elena Martinez (contingent) must NOT be included in allocation (mention of her exclusion is acceptable)\")\n\n    # Check Analyst/Date attribution: \"analyst\" and a date must appear within 80 characters of each other\n    analyst_match = re.search(r'analyst', ct_lower)\n    # Look for date patterns: 2026-04-10, April 10, 2026, 04/10/2026, 2026, etc.\n    date_matches = list(re.finditer(r'(?:202[0-9][-/]\\d{1,2}[-/]\\d{1,2}|\\d{1,2}[-/]\\d{1,2}[-/]202[0-9]|(?:january|february|march|april|may|june|july|august|september|october|november|december)\\s+\\d{1,2},?\\s*202[0-9]|\\b202[0-9]\\b)', ct_lower))\n    \n    analyst_date_found = False\n    if analyst_match and date_matches:\n        analyst_pos = analyst_match.start()\n        for dm in date_matches:\n            date_pos = dm.start()\n            if abs(analyst_pos - date_pos) <= 80:\n                analyst_date_found = True\n                break\n\n    if not analyst_date_found:\n        failures.append(\"Missing Analyst/Date attribution (\\\"analyst\\\" and a date must appear within 80 characters of each other)\")\n\n    if failures:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-1 allocation comment issues: \" + \"; \".join(failures)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"CLM_LIFE-1 allocation comment is correct: Policy LC-2019-08844, $400,000, Maria 60%/$240k, David 40%/$160k, no Elena as beneficiary, has analyst/date attribution within 80 chars\",\n    }\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "819c155d-7a13-4695-978e-f567c5be3cc9",
    "sort_order": 2,
    "rubric_text": "In `jira_state.json`, CLM_LIFE-2 must NOT have CLM-910 or CLM-920 set, and must have a structured beneficiary allocation comment containing Policy LC-2021-15290, Face Amount $250,000.00, Thomas P. Whitfield (Primary, 50%, $125,000.00), Emily J. Whitfield (Primary, 50%, $125,000.00), without contingent beneficiary James R. Whitfield (mention of his exclusion is acceptable).",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\n\ndef extract_text_from_adf(node):\n    \"\"\"Recursively extract plain text from Atlassian Document Format.\"\"\"\n    text = \"\"\n    if isinstance(node, dict):\n        if node.get(\"type\") == \"text\":\n            text += node.get(\"text\", \"\")\n        for child in node.get(\"content\", []):\n            text += extract_text_from_adf(child)\n    elif isinstance(node, list):\n        for item in node:\n            text += extract_text_from_adf(item)\n    return text\n\n\ndef get_all_comment_text(data, issue_key):\n    \"\"\"Get all comment text for an issue from both top-level and inline locations.\"\"\"\n    text = \"\"\n    issue_key_upper = issue_key.upper()\n    # Top-level comments dict\n    for ck, cv in data.get(\"comments\", {}).items():\n        if ck.upper() == issue_key_upper:\n            if isinstance(cv, list):\n                for comment in cv:\n                    body = comment.get(\"body\", {}) if isinstance(comment, dict) else comment\n                    if isinstance(body, str):\n                        text += body + \"\\n\"\n                    else:\n                        text += extract_text_from_adf(body) + \"\\n\"\n    # Inline in issue fields\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    issue = None\n    if isinstance(issues, dict):\n        for k, v in issues.items():\n            if k.upper() == issue_key_upper:\n                issue = v\n                break\n    if isinstance(issues, list):\n        for item in issues:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == issue_key_upper:\n                issue = item\n                break\n    if issue and isinstance(issue, dict):\n        fields = issue.get(\"fields\", {})\n        if isinstance(fields, dict):\n            comment_field = fields.get(\"comment\", {})\n            if isinstance(comment_field, dict):\n                for c in comment_field.get(\"comments\", []):\n                    body = c.get(\"body\", {}) if isinstance(c, dict) else c\n                    if isinstance(body, str):\n                        text += body + \"\\n\"\n                    else:\n                        text += extract_text_from_adf(body) + \"\\n\"\n    return text\n\n\ndef find_issue(data, issue_key):\n    \"\"\"Find an issue by key (case-insensitive) in various data shapes.\"\"\"\n    issue_key_upper = issue_key.upper()\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    if isinstance(issues, dict):\n        for key, val in issues.items():\n            if key.upper() == issue_key_upper:\n                return val\n    if isinstance(issues, list):\n        for item in issues:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == issue_key_upper:\n                return item\n    if isinstance(data, list):\n        for item in data:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == issue_key_upper:\n                return item\n    return None\n\n\ndef get_status_name(issue):\n    if not isinstance(issue, dict):\n        return \"\"\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        sf = fields.get(\"status\")\n        if isinstance(sf, dict):\n            return str(sf.get(\"name\", \"\")).strip()\n        elif isinstance(sf, str):\n            return sf.strip()\n    ts = issue.get(\"status\")\n    if isinstance(ts, dict):\n        return str(ts.get(\"name\", \"\")).strip()\n    elif isinstance(ts, str):\n        return ts.strip()\n    return \"\"\n\n\ndef check_routed(issue, status_lower):\n    \"\"\"Check if the issue has been transitioned to 'Route to AP for Payout'.\"\"\"\n    if \"route to ap for payout\" in status_lower:\n        return True\n    # Check transitions history\n    transitions = issue.get(\"transitions\", [])\n    if isinstance(transitions, list):\n        for t in transitions:\n            t_name = \"\"\n            if isinstance(t, dict):\n                t_name = str(t.get(\"name\", \"\")).lower()\n                to_field = t.get(\"to\", {})\n                if isinstance(to_field, dict):\n                    t_name += \" \" + str(to_field.get(\"name\", \"\")).lower()\n                elif isinstance(to_field, str):\n                    t_name += \" \" + to_field.lower()\n            elif isinstance(t, str):\n                t_name = t.lower()\n            if \"route to ap for payout\" in t_name:\n                return True\n    # Check changelog\n    changelog = issue.get(\"changelog\", {})\n    if isinstance(changelog, dict):\n        histories = changelog.get(\"histories\", [])\n        if isinstance(histories, list):\n            for h in histories:\n                for item in (h.get(\"items\", []) if isinstance(h, dict) else []):\n                    if isinstance(item, dict) and str(item.get(\"field\", \"\")).lower() == \"status\":\n                        to_str = str(item.get(\"toString\", \"\")).lower()\n                        if \"route to ap for payout\" in to_str:\n                            return True\n    return False\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    issue = find_issue(data, \"CLM_LIFE-2\")\n    if issue is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-2 not found in jira_state.json\"}\n\n    status_name = get_status_name(issue)\n    status_lower = status_name.lower()\n\n    # --- Extract labels ---\n    labels = []\n    fields = issue.get(\"fields\", {}) if isinstance(issue, dict) else {}\n    if isinstance(fields, dict):\n        raw_labels = fields.get(\"labels\", [])\n        if isinstance(raw_labels, list):\n            labels = [str(l).lower() for l in raw_labels]\n\n    # Serialize all field values for broad matching\n    all_field_values_str = json.dumps(fields).lower() if isinstance(fields, dict) else \"\"\n    # Also serialize whole issue for broader check\n    all_issue_str = json.dumps(issue).lower() if isinstance(issue, dict) else \"\"\n\n    # --- Check CLM-910 NOT set ---\n    has_910 = (\n        \"clm-910\" in status_lower\n        or \"identity verification hold\" in status_lower\n        or any(\"clm-910\" in lbl for lbl in labels)\n        or any(\"identity-verification-hold\" in lbl or \"identity verification hold\" in lbl for lbl in labels)\n        or \"clm-910\" in all_field_values_str\n    )\n\n    # --- Check CLM-920 NOT set ---\n    has_920 = (\n        \"clm-920\" in status_lower\n        or \"complex beneficiary hold\" in status_lower\n        or any(\"clm-920\" in lbl for lbl in labels)\n        or any(\"complex-beneficiary-hold\" in lbl or \"complex beneficiary hold\" in lbl for lbl in labels)\n        or \"clm-920\" in all_field_values_str\n    )\n\n    # --- Check status is \"Route to AP for Payout\" ---\n    routed = check_routed(issue, status_lower)\n\n    # --- Get all comment text (ADF-aware) ---\n    comments_text = get_all_comment_text(data, \"CLM_LIFE-2\")\n    ct_lower = comments_text.lower()\n\n    failures = []\n\n    # Hold checks\n    if has_910:\n        failures.append(f\"CLM-910 Identity Verification Hold is set (status: '{status_name}')\")\n    if has_920:\n        failures.append(f\"CLM-920 Complex Beneficiary Hold is set (status: '{status_name}')\")\n    # Allocation comment checks - look for beneficiary allocation or similar structured comment\n    has_alloc_header = (\n        \"beneficiary\" in ct_lower\n        or \"allocation\" in ct_lower\n        or \"payout\" in ct_lower\n    )\n    if not has_alloc_header:\n        failures.append(\"Missing structured beneficiary allocation comment\")\n\n    # Policy number\n    if \"lc-2021-15290\" not in ct_lower and \"lc202115290\" not in ct_lower and \"lc 2021 15290\" not in ct_lower:\n        failures.append(\"Missing Policy LC-2021-15290 in comments\")\n\n    # Face amount $250,000.00\n    if \"250,000\" not in comments_text and \"250000\" not in comments_text and \"250,000.00\" not in comments_text:\n        failures.append(\"Missing Face Amount $250,000.00 in comments\")\n\n    # Thomas P. Whitfield\n    has_thomas = \"thomas\" in ct_lower and \"whitfield\" in ct_lower\n    if not has_thomas:\n        failures.append(\"Missing beneficiary Thomas P. Whitfield in comments\")\n\n    # Emily J. Whitfield\n    has_emily = \"emily\" in ct_lower and \"whitfield\" in ct_lower\n    if not has_emily:\n        failures.append(\"Missing beneficiary Emily J. Whitfield in comments\")\n\n    # Check 50% split - accept various representations\n    has_50 = bool(re.search(r'50\\s*%', comments_text) or '50%' in comments_text or '50.00%' in comments_text or '50.0%' in comments_text)\n    if not has_50:\n        failures.append(\"Missing 50% split for beneficiaries\")\n\n    # Check $125,000.00 amount\n    if \"125,000\" not in comments_text and \"125000\" not in comments_text:\n        failures.append(\"Missing $125,000.00 allocation amount\")\n\n    # Check Primary designation\n    if \"primary\" not in ct_lower:\n        failures.append(\"Missing 'Primary' designation for beneficiaries\")\n\n    # James R. Whitfield must NOT be included in the allocation as a payee\n    # Mention of his exclusion is acceptable per rubric\n    if \"james\" in ct_lower and \"whitfield\" in ct_lower:\n        james_as_payee = False\n        lines = comments_text.split('\\n')\n        for line in lines:\n            line_lower = line.lower()\n            if 'james' in line_lower and 'whitfield' in line_lower:\n                exclusion_words = ['exclud', 'contingent', 'not eligible', 'not included', 'n/a', 'skip',\n                                   'omit', 'no payout', '$0', '0.00', '0%', 'not applicable',\n                                   'does not', 'do not', 'not payable', 'no allocation',\n                                   'waiv', 'none', 'excluded', 'ineligible']\n                has_exclusion = any(ew in line_lower for ew in exclusion_words)\n                # Check if line has a real dollar allocation for James\n                has_dollar = bool(re.search(r'\\$[1-9][0-9,]+', line))\n                if has_dollar and not has_exclusion:\n                    james_as_payee = True\n\n        if james_as_payee:\n            failures.append(\"James R. Whitfield (contingent) must NOT be included in allocation as a payee\")\n\n    if failures:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-2 issues: \" + \"; \".join(failures)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"CLM_LIFE-2: No CLM-910/CLM-920 holds, correct structured beneficiary allocation comment with Policy LC-2021-15290, Face Amount $250,000.00, Thomas P. Whitfield (Primary, 50%, $125,000.00), Emily J. Whitfield (Primary, 50%, $125,000.00), James R. Whitfield contingent properly excluded.\",\n    }\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "3c204513-4ad4-418b-b9a7-a5efa2ced421",
    "sort_order": 3,
    "rubric_text": "In `jira_state.json`, CLM_LIFE-3 must NOT have CLM-910 or CLM-920 set, and must have a structured beneficiary allocation comment with pro-rata redistribution: Mark H. Drummond (63.64% [allow 63.6%], $381,818.18) and Susan E. Drummond (36.36% [allow 36.3%], $218,181.82), excluding deceased Linda M. Drummond and contingent George T. Drummond. Linda and George mentioned as excluded or not receiving payout is acceptable.",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\n\ndef extract_text_from_adf(node):\n    \"\"\"Recursively extract plain text from Atlassian Document Format.\n\n    Block-level nodes (paragraph, heading, list item, etc.) are separated with\n    newlines, and hardBreak nodes emit a newline, so the per-line payout checks\n    below see the same line boundaries the comment author intended (ADF\n    otherwise drops them, collapsing the whole comment onto one line).\n    \"\"\"\n    block_types = {\"paragraph\", \"heading\", \"blockquote\", \"listItem\",\n                   \"bulletList\", \"orderedList\", \"codeBlock\", \"rule\", \"tableRow\"}\n    text = \"\"\n    if isinstance(node, dict):\n        node_type = node.get(\"type\")\n        if node_type == \"text\":\n            text += node.get(\"text\", \"\")\n        elif node_type == \"hardBreak\":\n            text += \"\\n\"\n        for child in node.get(\"content\", []):\n            text += extract_text_from_adf(child)\n        if node_type in block_types:\n            text += \"\\n\"\n    elif isinstance(node, list):\n        for item in node:\n            text += extract_text_from_adf(item)\n    return text\n\n\ndef get_all_comment_text(data, issue_key):\n    \"\"\"Get all comment text for an issue from both top-level and inline locations.\"\"\"\n    text = \"\"\n    # Top-level comments keyed by issue\n    for comment in data.get(\"comments\", {}).get(issue_key, []):\n        body = comment.get(\"body\", {})\n        if isinstance(body, str):\n            text += body + \"\\n\"\n        else:\n            text += extract_text_from_adf(body) + \"\\n\"\n    # Inline comments inside issues -> fields -> comment\n    issues = data.get(\"issues\", {})\n    issue = issues.get(issue_key, {}) if isinstance(issues, dict) else {}\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        comment_field = fields.get(\"comment\", {})\n        if isinstance(comment_field, dict):\n            for c in comment_field.get(\"comments\", []):\n                body = c.get(\"body\", {})\n                if isinstance(body, str):\n                    text += body + \"\\n\"\n                else:\n                    text += extract_text_from_adf(body) + \"\\n\"\n    return text\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    # --- Locate CLM_LIFE-3 ---\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    issue = None\n    if isinstance(issues, dict):\n        issue = issues.get(\"CLM_LIFE-3\")\n        if issue is None:\n            for key, val in issues.items():\n                if key.upper() == \"CLM_LIFE-3\":\n                    issue = val\n                    break\n    if issue is None and isinstance(data, list):\n        for item in data:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == \"CLM_LIFE-3\":\n                issue = item\n                break\n\n    if issue is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-3 not found in jira_state.json\"}\n\n    # --- Extract status name ---\n    def get_status_name(iss):\n        fields = iss.get(\"fields\", {})\n        if isinstance(fields, dict):\n            sf = fields.get(\"status\")\n            if isinstance(sf, dict):\n                return str(sf.get(\"name\", \"\")).strip()\n            elif isinstance(sf, str):\n                return sf.strip()\n        ts = iss.get(\"status\")\n        if isinstance(ts, dict):\n            return str(ts.get(\"name\", \"\")).strip()\n        elif isinstance(ts, str):\n            return ts.strip()\n        return \"\"\n\n    status_name = get_status_name(issue)\n    status_lower = status_name.lower()\n\n    # --- Extract labels ---\n    labels = []\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        raw_labels = fields.get(\"labels\", [])\n        if isinstance(raw_labels, list):\n            labels = [str(l).lower() for l in raw_labels]\n\n    # --- Check CLM-910 NOT set (status and labels) ---\n    has_910 = (\n        \"clm-910\" in status_lower\n        or \"identity verification hold\" in status_lower\n        or any(\"clm-910\" in lbl for lbl in labels)\n        or any(\"identity-verification-hold\" in lbl or \"identity verification hold\" in lbl for lbl in labels)\n    )\n\n    # --- Check CLM-920 NOT set (status and labels) ---\n    has_920 = (\n        \"clm-920\" in status_lower\n        or \"complex beneficiary hold\" in status_lower\n        or any(\"clm-920\" in lbl for lbl in labels)\n        or any(\"complex-beneficiary-hold\" in lbl or \"complex beneficiary hold\" in lbl for lbl in labels)\n    )\n\n    # --- Check status is \"Route to AP for Payout\" ---\n    routed = \"route to ap for payout\" in status_lower\n    if not routed:\n        transitions = issue.get(\"transitions\", [])\n        if isinstance(transitions, list):\n            for t in transitions:\n                t_name = \"\"\n                if isinstance(t, dict):\n                    t_name = str(t.get(\"name\", t.get(\"to\", \"\"))).lower()\n                elif isinstance(t, str):\n                    t_name = t.lower()\n                if \"route to ap for payout\" in t_name:\n                    routed = True\n                    break\n\n    # --- Get all comment text (ADF-aware) ---\n    comments_text = get_all_comment_text(data, \"CLM_LIFE-3\")\n    ct_lower = comments_text.lower()\n\n    # --- Extract allocation block only (text from the allocation header onward) ---\n    # This scopes George/Linda payout checks to the actual allocation table,\n    # preventing false positives from policy schedule documentation in Step 1 review.\n    alloc_header_pos = ct_lower.find(\"beneficiary allocation\")\n    if alloc_header_pos == -1:\n        # Fallback: search for any structured payout block marker\n        alloc_header_pos = ct_lower.find(\"beneficiary 1\")\n    alloc_block = ct_lower[alloc_header_pos:] if alloc_header_pos != -1 else \"\"\n\n    failures = []\n\n    # Hold checks\n    if has_910:\n        failures.append(f\"CLM-910 Identity Verification Hold is set (status: '{status_name}')\")\n    if has_920:\n        failures.append(f\"CLM-920 Complex Beneficiary Hold is set (status: '{status_name}')\")\n    # --- Beneficiary allocation comment checks ---\n    has_alloc_header = (\"beneficiary allocation\" in ct_lower or\n                        (\"beneficiary\" in ct_lower and \"allocation\" in ct_lower) or\n                        (\"beneficiary\" in ct_lower and (\"redistribution\" in ct_lower or \"pro-rata\" in ct_lower or \"pro rata\" in ct_lower)))\n    if not has_alloc_header:\n        failures.append(\"Missing 'Beneficiary Allocation' or similar structured header in comments\")\n\n    # Mark H. Drummond — redistributed to ~63.64%, ~$381,818.18\n    has_mark = \"mark\" in ct_lower and \"drummond\" in ct_lower\n    if not has_mark:\n        failures.append(\"Missing Mark H. Drummond in comments\")\n    else:\n        mark_amount_ok = bool(re.search(r\"381[,.]?818\", comments_text))\n        if not mark_amount_ok:\n            failures.append(\"Missing Mark Drummond redistributed amount ~$381,818.18\")\n        mark_pct_ok = bool(re.search(r\"63\\.6[0-9]*\", comments_text))\n        if not mark_pct_ok:\n            failures.append(\"Missing Mark Drummond redistributed percentage ~63.64%\")\n\n    # Susan E. Drummond — redistributed to ~36.36%, ~$218,181.82\n    has_susan = \"susan\" in ct_lower and \"drummond\" in ct_lower\n    if not has_susan:\n        failures.append(\"Missing Susan E. Drummond in comments\")\n    else:\n        susan_amount_ok = bool(re.search(r\"218[,.]?181\", comments_text))\n        if not susan_amount_ok:\n            failures.append(\"Missing Susan Drummond redistributed amount ~$218,181.82\")\n        susan_pct_ok = bool(re.search(r\"36\\.3[0-9]*\", comments_text))\n        if not susan_pct_ok:\n            failures.append(\"Missing Susan Drummond redistributed percentage ~36.36%\")\n\n    # --- Forbidden payees: George T. Drummond (contingent) and Linda M. Drummond\n    #     (deceased) must NOT appear as a payee RECEIVING a payout in the allocation. ---\n    # Mentioning either as excluded / not listed / deceased / redistributed is fine.\n    # We scan line by line (extract_text_from_adf newline-separates block nodes) and\n    # only fail when the name shares a line with a NON-ZERO dollar amount and that line\n    # carries no exclusion language -- i.e. an actual payout row, not a free-text note\n    # or the grand total. The non-zero requirement ($[1-9]...) ignores \"$0.00\" lines.\n    # alloc_block is already lower-cased, so name/exclusion tokens are compared as-is.\n    exclusion_words = [\n        \"exclud\", \"not listed\", \"not included\", \"not receiving\", \"not eligible\",\n        \"ineligible\", \"redistribut\", \"deceased\", \"predecease\", \"n/a\",\n        \"not applicable\", \"not payable\", \"no payout\", \"no allocation\",\n    ]\n\n    def receives_payout(first_name):\n        for line in alloc_block.split(\"\\n\"):\n            if first_name in line and \"drummond\" in line:\n                has_exclusion = any(w in line for w in exclusion_words)\n                has_nonzero_amount = bool(re.search(r\"\\$\\s*[1-9][\\d,]*\", line))\n                if has_nonzero_amount and not has_exclusion:\n                    return True\n        return False\n\n    if receives_payout(\"george\"):\n        failures.append(\"George T. Drummond (contingent) must NOT be included in allocation with a payout amount\")\n    if receives_payout(\"linda\"):\n        failures.append(\"Linda M. Drummond (deceased) must NOT be included in allocation with a payout amount\")\n\n    if failures:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-3 issues: \" + \"; \".join(failures)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": (\n            \"CLM_LIFE-3: No CLM-910/CLM-920 holds, transitioned to 'Route to AP for Payout', \"\n            \"correct pro-rata redistribution comment — Mark H. Drummond ~63.64%/$381,818.18, \"\n            \"Susan E. Drummond ~36.36%/$218,181.82, deceased Linda M. Drummond and contingent \"\n            \"George T. Drummond excluded from allocation. All checks passed.\"\n        ),\n    }",
    "criterion_type": "expected_output"
  },
  {
    "id": "d2411716-ed63-4513-8756-51f430561e46",
    "sort_order": 4,
    "rubric_text": "In `jira_state.json`, CLM_LIFE-4 must NOT have CLM-910 or CLM-920 set, and must have a structured beneficiary allocation comment containing Policy LC-2020-11467, Face Amount $350,000.00, Emeka A. Okafor (Primary, 70%, $245,000.00), Chidi E. Okafor (Primary, 30%, $105,000.00), without contingent beneficiary Ngozi F. Okafor (mention of her exclusion is acceptable).",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\n\ndef extract_text_from_adf(node):\n    \"\"\"Recursively extract plain text from Atlassian Document Format.\"\"\"\n    text = \"\"\n    if isinstance(node, dict):\n        if node.get(\"type\") == \"text\":\n            text += node.get(\"text\", \"\")\n        for child in node.get(\"content\", []):\n            text += extract_text_from_adf(child)\n    elif isinstance(node, list):\n        for item in node:\n            text += extract_text_from_adf(item)\n    return text\n\n\ndef get_all_comment_text(data, issue_key):\n    \"\"\"Get all comment text for an issue from both top-level and inline locations.\"\"\"\n    text = \"\"\n    # Top-level comments dict\n    for comment in data.get(\"comments\", {}).get(issue_key, []):\n        body = comment.get(\"body\", {})\n        if isinstance(body, str):\n            text += body + \"\\n\"\n        else:\n            text += extract_text_from_adf(body) + \"\\n\"\n    # Inline comments in issue fields\n    issues = data.get(\"issues\", {})\n    issue = issues.get(issue_key, {}) if isinstance(issues, dict) else {}\n    fields = issue.get(\"fields\", {}) if isinstance(issue, dict) else {}\n    if isinstance(fields, dict):\n        comment_field = fields.get(\"comment\", {})\n        if isinstance(comment_field, dict):\n            for c in comment_field.get(\"comments\", []):\n                body = c.get(\"body\", {})\n                if isinstance(body, str):\n                    text += body + \"\\n\"\n                else:\n                    text += extract_text_from_adf(body) + \"\\n\"\n    return text\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    # --- Locate CLM_LIFE-4 ---\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    issue = None\n    if isinstance(issues, dict):\n        issue = issues.get(\"CLM_LIFE-4\")\n        if issue is None:\n            for key, val in issues.items():\n                if key.upper() == \"CLM_LIFE-4\":\n                    issue = val\n                    break\n    if issue is None and isinstance(data, list):\n        for item in data:\n            if isinstance(item, dict) and item.get(\"key\", \"\").upper() == \"CLM_LIFE-4\":\n                issue = item\n                break\n\n    if issue is None:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-4 not found in jira_state.json\"}\n\n    # --- Extract status name ---\n    def get_status_name(iss):\n        fields = iss.get(\"fields\", {})\n        if isinstance(fields, dict):\n            sf = fields.get(\"status\")\n            if isinstance(sf, dict):\n                return str(sf.get(\"name\", \"\")).strip()\n            elif isinstance(sf, str):\n                return sf.strip()\n        ts = iss.get(\"status\")\n        if isinstance(ts, dict):\n            return str(ts.get(\"name\", \"\")).strip()\n        elif isinstance(ts, str):\n            return ts.strip()\n        return \"\"\n\n    status_name = get_status_name(issue)\n    status_lower = status_name.lower()\n\n    # --- Extract labels ---\n    labels = []\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        raw_labels = fields.get(\"labels\", [])\n        if isinstance(raw_labels, list):\n            labels = [str(l).lower() for l in raw_labels]\n\n    # --- Collect all text fields from the issue for hold detection ---\n    def all_field_values_str(iss):\n        \"\"\"Gather string representations of all field values for broad searching.\"\"\"\n        parts = []\n        flds = iss.get(\"fields\", {})\n        if isinstance(flds, dict):\n            for k, v in flds.items():\n                if k == \"comment\":\n                    continue  # skip comments for hold detection\n                if isinstance(v, str):\n                    parts.append(v.lower())\n                elif isinstance(v, dict):\n                    parts.append(json.dumps(v).lower())\n                elif isinstance(v, list):\n                    parts.append(json.dumps(v).lower())\n        return \" \".join(parts)\n\n    all_fields_text = all_field_values_str(issue)\n\n    # --- Check CLM-910 NOT set ---\n    has_910 = (\n        \"clm-910\" in status_lower\n        or \"identity verification hold\" in status_lower\n        or any(\"clm-910\" in lbl for lbl in labels)\n        or any(\"identity-verification-hold\" in lbl or \"identity verification hold\" in lbl for lbl in labels)\n        or \"clm-910\" in all_fields_text\n    )\n\n    # --- Check CLM-920 NOT set ---\n    has_920 = (\n        \"clm-920\" in status_lower\n        or \"complex beneficiary hold\" in status_lower\n        or any(\"clm-920\" in lbl for lbl in labels)\n        or any(\"complex-beneficiary-hold\" in lbl or \"complex beneficiary hold\" in lbl for lbl in labels)\n        or \"clm-920\" in all_fields_text\n    )\n\n    # --- Check status is \"Route to AP for Payout\" ---\n    routed = \"route to ap for payout\" in status_lower\n    if not routed:\n        # Also check transitions history\n        transitions = issue.get(\"transitions\", [])\n        if isinstance(transitions, list):\n            for t in transitions:\n                t_name = \"\"\n                if isinstance(t, dict):\n                    t_name = str(t.get(\"name\", t.get(\"to\", \"\"))).lower()\n                elif isinstance(t, str):\n                    t_name = t.lower()\n                if \"route to ap for payout\" in t_name:\n                    routed = True\n                    break\n\n    # --- Get all comment text (ADF-aware) ---\n    comments_text = get_all_comment_text(data, \"CLM_LIFE-4\")\n    ct_lower = comments_text.lower()\n\n    failures = []\n\n    # Hold checks\n    if has_910:\n        failures.append(f\"CLM-910 Identity Verification Hold is set (status: '{status_name}')\")\n    if has_920:\n        failures.append(f\"CLM-920 Complex Beneficiary Hold is set (status: '{status_name}')\")\n    # Allocation comment checks - be loose about the header\n    has_alloc_header = (\n        \"beneficiary allocation\" in ct_lower\n        or \"allocation\" in ct_lower\n        or \"beneficiary\" in ct_lower\n    )\n    if not has_alloc_header:\n        failures.append(\"Missing 'Beneficiary Allocation' or similar header in comments\")\n\n    if \"lc-2020-11467\" not in ct_lower and \"lc202011467\" not in ct_lower:\n        failures.append(\"Missing Policy LC-2020-11467\")\n\n    if \"350,000\" not in comments_text and \"350000\" not in comments_text and \"350,000.00\" not in comments_text:\n        failures.append(\"Missing Face Amount $350,000.00\")\n\n    # Emeka A. Okafor\n    if \"emeka\" not in ct_lower or \"okafor\" not in ct_lower:\n        failures.append(\"Missing beneficiary Emeka A. Okafor\")\n    else:\n        if \"245,000\" not in comments_text and \"245000\" not in comments_text and \"245,000.00\" not in comments_text:\n            failures.append(\"Missing Emeka Okafor amount $245,000.00\")\n        if \"70%\" not in comments_text and \"70 %\" not in comments_text and \"70.0%\" not in comments_text:\n            failures.append(\"Missing Emeka Okafor 70% split\")\n\n    # Chidi E. Okafor\n    if \"chidi\" not in ct_lower:\n        failures.append(\"Missing beneficiary Chidi E. Okafor\")\n    else:\n        if \"105,000\" not in comments_text and \"105000\" not in comments_text and \"105,000.00\" not in comments_text:\n            failures.append(\"Missing Chidi Okafor amount $105,000.00\")\n        if \"30%\" not in comments_text and \"30 %\" not in comments_text and \"30.0%\" not in comments_text:\n            failures.append(\"Missing Chidi Okafor 30% split\")\n\n    # Ngozi F. Okafor must NOT be included as an allocated beneficiary.\n    # Mention of her exclusion (e.g., \"Ngozi excluded as contingent\") is acceptable.\n    # We check if Ngozi appears with a dollar amount or percentage that looks like an allocation.\n    if \"ngozi\" in ct_lower:\n        # Look for Ngozi paired with allocation indicators (dollar amounts or percentages)\n        # but allow exclusion language\n        ngozi_sections = [m.start() for m in re.finditer(r\"ngozi\", ct_lower)]\n        for pos in ngozi_sections:\n            # Get surrounding context (200 chars)\n            context_start = max(0, pos - 100)\n            context_end = min(len(ct_lower), pos + 200)\n            context = ct_lower[context_start:context_end]\n            # Check if this mention has allocation numbers AND no exclusion language\n            has_exclusion_language = any(word in context for word in [\n                \"exclud\", \"not included\", \"not eligible\", \"contingent\",\n                \"skip\", \"omit\", \"n/a\", \"not applicable\", \"no payout\",\n                \"does not receive\", \"not a primary\", \"not primary\"\n            ])\n            has_alloc_numbers = bool(re.search(r\"\\$[\\d,]+\\.?\\d*\", context)) and not has_exclusion_language\n            has_pct = bool(re.search(r\"\\d+\\.?\\d*\\s*%\", context)) and not has_exclusion_language\n            if (has_alloc_numbers or has_pct) and not has_exclusion_language:\n                failures.append(\"Ngozi F. Okafor (contingent) must NOT be included in beneficiary allocation with amounts/percentages\")\n                break\n\n    if failures:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-4 issues: \" + \"; \".join(failures)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"CLM_LIFE-4: No CLM-910/CLM-920 holds, correct structured beneficiary allocation with Policy LC-2020-11467, Face Amount $350,000.00, Emeka A. Okafor (Primary, 70%, $245,000.00), Chidi E. Okafor (Primary, 30%, $105,000.00), contingent Ngozi F. Okafor properly excluded from allocation.\",\n    }\n",
    "criterion_type": "expected_output"
  },
  {
    "id": "rubric_1775973366733",
    "sort_order": 5,
    "rubric_text": "CLM_LIFE-1 must NOT be transitioned to CLM_LEGAL or routed to Claims Supervisor (CLM-Identity).",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\n\ndef extract_text_from_adf(node):\n    \"\"\"Recursively extract plain text from Atlassian Document Format.\"\"\"\n    text = \"\"\n    if isinstance(node, dict):\n        if node.get(\"type\") == \"text\":\n            text += node.get(\"text\", \"\")\n        for child in node.get(\"content\", []):\n            text += extract_text_from_adf(child)\n    elif isinstance(node, list):\n        for item in node:\n            text += extract_text_from_adf(item)\n    return text\n\n\ndef get_all_comment_text(data, issue_key):\n    \"\"\"Get all comment text for an issue from both top-level and inline locations.\"\"\"\n    text = \"\"\n    for comment in data.get(\"comments\", {}).get(issue_key, []):\n        body = comment.get(\"body\", {})\n        text += extract_text_from_adf(body) + \"\\n\"\n    issues = data.get(\"issues\", {})\n    issue = issues.get(issue_key, {}) if isinstance(issues, dict) else {}\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        comment_field = fields.get(\"comment\", {})\n        if isinstance(comment_field, dict):\n            for c in comment_field.get(\"comments\", []):\n                body = c.get(\"body\", {})\n                text += extract_text_from_adf(body) + \"\\n\"\n    return text\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    failures = []\n\n    # --- Check 1: Were any new CLM_LEGAL issues created that reference CLM_LIFE-1? ---\n    # CLM_LEGAL-1 is pre-existing (Queue Roles & Assignees), so skip it\n    for key, val in issues.items():\n        if \"legal\" in key.lower() and key != \"CLM_LEGAL-1\":\n            issue_str = json.dumps(val)\n            if \"CLM_LIFE-1\" in issue_str or \"Martinez\" in issue_str or \"LC-2019-08844\" in issue_str:\n                summary = \"\"\n                if isinstance(val, dict):\n                    fields = val.get(\"fields\", {})\n                    if isinstance(fields, dict):\n                        summary = str(fields.get(\"summary\", \"\"))\n                failures.append(f\"New CLM_LEGAL issue '{key}' was created referencing CLM_LIFE-1 (summary: '{summary}')\")\n\n    # --- Check 2: Did the model's comments on CLM_LIFE-1 indicate routing to Legal? ---\n    comments_text = get_all_comment_text(data, \"CLM_LIFE-1\")\n    ct_lower = comments_text.lower()\n\n    routed_to_legal = False\n    if \"clm-legal\" in ct_lower:\n        legal_patterns = [\n            r\"rout\\w*\\s+to\\s+clm[- ]?legal\",\n            r\"set\\s+clm[- ]?legal\",\n            r\"created?\\s+.*clm[- ]?legal\",\n            r\"assign\\w*\\s+.*clm[- ]?legal\",\n            r\"forward\\w*\\s+.*clm[- ]?legal\",\n            r\"escala\\w*\\s+.*clm[- ]?legal\",\n        ]\n        for pat in legal_patterns:\n            if re.search(pat, ct_lower):\n                routed_to_legal = True\n                break\n\n    if routed_to_legal:\n        failures.append(\"CLM_LIFE-1 comments indicate routing to CLM_LEGAL\")\n\n    # --- Check 3: Is the status itself set to a Legal queue? ---\n    issue = issues.get(\"CLM_LIFE-1\")\n    if issue is not None:\n        fields = issue.get(\"fields\", {})\n        status_name = \"\"\n        if isinstance(fields, dict):\n            sf = fields.get(\"status\")\n            if isinstance(sf, dict):\n                status_name = str(sf.get(\"name\", \"\")).strip()\n            elif isinstance(sf, str):\n                status_name = sf.strip()\n\n        if \"legal\" in status_name.lower():\n            failures.append(f\"CLM_LIFE-1 status is set to a Legal queue ('{status_name}')\")\n\n    if failures:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-1 incorrectly routed to Legal: \" + \"; \".join(failures)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"CLM_LIFE-1: Not routed to CLM_LEGAL, no Legal issues created for this claim\",\n    }",
    "criterion_type": "incorrect_behavior"
  },
  {
    "id": "rubric_1775973378903",
    "sort_order": 6,
    "rubric_text": "CLM_LIFE-4 must NOT be transitioned to CLM_LEGAL or have any hold status applied.",
    "verifier_code": "from pathlib import Path\nimport json\nimport re\n\n\ndef extract_text_from_adf(node):\n    \"\"\"Recursively extract plain text from Atlassian Document Format.\"\"\"\n    text = \"\"\n    if isinstance(node, dict):\n        if node.get(\"type\") == \"text\":\n            text += node.get(\"text\", \"\")\n        for child in node.get(\"content\", []):\n            text += extract_text_from_adf(child)\n    elif isinstance(node, list):\n        for item in node:\n            text += extract_text_from_adf(item)\n    return text\n\n\ndef get_all_comment_text(data, issue_key):\n    \"\"\"Get all comment text for an issue from both top-level and inline locations.\"\"\"\n    text = \"\"\n    for comment in data.get(\"comments\", {}).get(issue_key, []):\n        body = comment.get(\"body\", {})\n        text += extract_text_from_adf(body) + \"\\n\"\n    issues = data.get(\"issues\", {})\n    issue = issues.get(issue_key, {}) if isinstance(issues, dict) else {}\n    fields = issue.get(\"fields\", {})\n    if isinstance(fields, dict):\n        comment_field = fields.get(\"comment\", {})\n        if isinstance(comment_field, dict):\n            for c in comment_field.get(\"comments\", []):\n                body = c.get(\"body\", {})\n                text += extract_text_from_adf(body) + \"\\n\"\n    return text\n\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 jira_state.json\"}\n\n    jira_path = Path(external_services_path) / \"jira_state.json\"\n    if not jira_path.exists():\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"jira_state.json not found\"}\n\n    with open(jira_path) as f:\n        data = json.load(f)\n\n    issues = data.get(\"issues\", {}) if isinstance(data, dict) else {}\n    failures = []\n\n    # --- Check 1: Were any new CLM_LEGAL issues created that reference CLM_LIFE-4? ---\n    for key, val in issues.items():\n        if \"legal\" in key.lower() and key != \"CLM_LEGAL-1\":\n            issue_str = json.dumps(val)\n            if \"CLM_LIFE-4\" in issue_str or \"Okafor\" in issue_str or \"LC-2020-11467\" in issue_str:\n                summary = \"\"\n                if isinstance(val, dict):\n                    fields = val.get(\"fields\", {})\n                    if isinstance(fields, dict):\n                        summary = str(fields.get(\"summary\", \"\"))\n                failures.append(f\"New CLM_LEGAL issue '{key}' was created referencing CLM_LIFE-4 (summary: '{summary}')\")\n\n    # --- Check 2: Did the model's comments on CLM_LIFE-4 indicate routing to Legal? ---\n    comments_text = get_all_comment_text(data, \"CLM_LIFE-4\")\n    ct_lower = comments_text.lower()\n\n    routed_to_legal = False\n    if \"clm-legal\" in ct_lower:\n        legal_patterns = [\n            r\"rout\\w*\\s+to\\s+clm[- ]?legal\",\n            r\"set\\s+clm[- ]?legal\",\n            r\"created?\\s+.*clm[- ]?legal\",\n            r\"assign\\w*\\s+.*clm[- ]?legal\",\n            r\"forward\\w*\\s+.*clm[- ]?legal\",\n            r\"escala\\w*\\s+.*clm[- ]?legal\",\n        ]\n        for pat in legal_patterns:\n            if re.search(pat, ct_lower):\n                routed_to_legal = True\n                break\n\n    if routed_to_legal:\n        failures.append(\"CLM_LIFE-4 comments indicate routing to CLM_LEGAL\")\n\n    # --- Check 3: Is the status itself set to a Legal queue? ---\n    issue = issues.get(\"CLM_LIFE-4\")\n    if issue is not None:\n        fields = issue.get(\"fields\", {})\n        status_name = \"\"\n        if isinstance(fields, dict):\n            sf = fields.get(\"status\")\n            if isinstance(sf, dict):\n                status_name = str(sf.get(\"name\", \"\")).strip()\n            elif isinstance(sf, str):\n                status_name = sf.strip()\n\n        if \"legal\" in status_name.lower():\n            failures.append(f\"CLM_LIFE-4 status is set to a Legal queue ('{status_name}')\")\n\n    if failures:\n        return {\"pass\": False, \"score\": 0.0, \"feedback\": \"CLM_LIFE-4 incorrectly routed to Legal: \" + \"; \".join(failures)}\n\n    return {\n        \"pass\": True,\n        \"score\": 1.0,\n        \"feedback\": \"CLM_LIFE-4: Not routed to CLM_LEGAL, no Legal issues created for this claim\",\n    }",
    "criterion_type": "incorrect_behavior"
  }
]
