{
  "schema": "ouro-outreach-transaction-fixtures/1.0",
  "description": "Sanitized, machine-readable fixture pack for the outreach transaction contract. Every contact address is synthetic and ends in .invalid (RFC 2606 reserved TLD). Controller CC addresses are resolved at runtime from configuration; placeholders are used here. No real CRM rows, names, Resend message ids, or thread content appear in this file. Timestamps and message ids are interpolation tokens (${...}) to be filled by the test harness.",
  "invariants": {
    "write_once_fields": [
      "first_outbound_at",
      "first_outbound_email_id",
      "date_sent"
    ],
    "write_once_rule": "Set exactly once, on the identified-to-sent first-send transition. Every later upsert (reply, follow-up, CRM-only update) must omit them entirely; a guard rejects any upsert that attempts to change them.",
    "required_ccs": [
      "controller.one@example.invalid",
      "controller.two@example.invalid"
    ],
    "cc_rule": "Both controller addresses are CC'd on every outbound email, in the cc field, never bcc. Real addresses are resolved from runtime config, not stored here.",
    "idempotency_key": {
      "format": "hermes:{contact_id}:{intent}:{trigger}",
      "max_length": 256,
      "intents": [
        "cold",
        "followup",
        "reply",
        "nudge"
      ],
      "trigger_rule": "Inbound message id for replies, 'first' for a cold send, or 'followup-{first_outbound_email_id}' for the one allowed follow-up.",
      "determinism": "Retries of the same logical email reuse the same key. A different email gets a different key. Never generate a random key."
    },
    "follow_up_sent": {
      "allowed_values": [
        "true",
        "false"
      ],
      "rule": "Exact lowercase strings only. Any other casing ('True', 'FALSE') or type (boolean) is rejected by the upsert guard; a stray value silently drops the row out of every triage query."
    },
    "status_lifecycle": [
      "identified",
      "drafted",
      "sent",
      "replied",
      "blocked",
      "no_reply",
      "do_not_contact"
    ],
    "daily_caps": {
      "total_outbound": 8,
      "cold_first_contacts": 4,
      "followups": 4,
      "same_artifact_followups": 2
    },
    "followup_windows_days": {
      "researcher": [
        7,
        12
      ],
      "sponsor": [
        14,
        21
      ]
    }
  },
  "cases": [
    {
      "case_id": "T1-first-send",
      "transaction": "first_send",
      "given": {
        "daily_outbound_count": 2,
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "name": "Dana Okonkwo",
          "type": "researcher",
          "institution": "Meridian Institute",
          "email": "dana.okonkwo@example.invalid",
          "focus": "Synthetic fixture: computational screening of layered magnets",
          "batch": "fixture-pack-1",
          "status": "identified",
          "follow_up_sent": "false",
          "reply_received": "false",
          "next_action": "Cold send drafted; send when channel available."
        }
      },
      "action": {
        "intent": "cold",
        "trigger": "first",
        "to": "dana.okonkwo@example.invalid",
        "subject": "Fixture subject line"
      },
      "expected": {
        "sends": 1,
        "idempotency_key": "hermes:c1a00000-0000-4000-8000-000000000001:cold:first",
        "cc": [
          "controller.one@example.invalid",
          "controller.two@example.invalid"
        ],
        "crm_after": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "status": "sent",
          "first_outbound_at": "${send_time}",
          "first_outbound_email_id": "${message_id}",
          "last_outbound_at": "${send_time}",
          "last_outbound_email_id": "${message_id}",
          "date_sent": "${send_date}",
          "email_id": "${message_id}",
          "reply_received": "false",
          "follow_up_sent": "false",
          "next_action": "Wait for reply; one follow-up allowed in the 7-12 day window."
        },
        "notes": "A CRM row missing first_outbound_email_id is treated as a first send regardless of any legacy hint. All six canonical first-send fields are written in this single transition."
      }
    },
    {
      "case_id": "T2-follow-up",
      "transaction": "follow_up",
      "given": {
        "daily_outbound_count": 3,
        "today": "2026-08-23",
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "name": "Dana Okonkwo",
          "type": "researcher",
          "email": "dana.okonkwo@example.invalid",
          "status": "sent",
          "reply_received": "false",
          "follow_up_sent": "false",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001",
          "last_outbound_at": "2026-08-15T14:00:00.000Z",
          "last_outbound_email_id": "mock-1001"
        }
      },
      "action": {
        "intent": "followup",
        "trigger": "followup-mock-1001",
        "to": "dana.okonkwo@example.invalid",
        "subject": "Fixture follow-up: something new"
      },
      "expected": {
        "sends": 1,
        "idempotency_key": "hermes:c1a00000-0000-4000-8000-000000000001:followup:followup-mock-1001",
        "cc": [
          "controller.one@example.invalid",
          "controller.two@example.invalid"
        ],
        "crm_after": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "status": "sent",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001",
          "last_outbound_at": "${send_time}",
          "last_outbound_email_id": "${message_id}",
          "email_id": "${message_id}",
          "follow_up_sent": "true",
          "next_action": "Follow-up sent. No further contact unless they reply."
        },
        "notes": "Eligibility: today inside 7-12 days after first_outbound_at, reply_received false, follow_up_sent false, follow-up carries something new. Write-once fields pass through unchanged."
      }
    },
    {
      "case_id": "T3-live-thread-reply",
      "transaction": "live_thread_reply",
      "given": {
        "daily_outbound_count": 5,
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "name": "Dana Okonkwo",
          "type": "researcher",
          "email": "dana.okonkwo@example.invalid",
          "status": "replied",
          "reply_received": "Replied ${inbound_time}: interested, asked for the dataset license.",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001",
          "last_outbound_at": "2026-08-15T14:00:00.000Z",
          "last_outbound_email_id": "mock-1001",
          "last_inbound_at": "${inbound_time}",
          "last_inbound_email_id": "mock-in-3001",
          "follow_up_sent": "true"
        },
        "precondition": "Full thread (sent and received) re-read in the same tick before drafting; duplicate guard passed; no controller outbound to the contact after the latest inbound."
      },
      "action": {
        "intent": "reply",
        "trigger": "mock-in-3001",
        "to": "dana.okonkwo@example.invalid",
        "subject": "Re: Fixture subject line"
      },
      "expected": {
        "sends": 1,
        "idempotency_key": "hermes:c1a00000-0000-4000-8000-000000000001:reply:mock-in-3001",
        "cc": [
          "controller.one@example.invalid",
          "controller.two@example.invalid"
        ],
        "crm_after": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "status": "replied",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001",
          "last_outbound_at": "${send_time}",
          "last_outbound_email_id": "${message_id}",
          "email_id": "${message_id}",
          "reply_received": "Replied ${inbound_time}: interested, asked for the dataset license. Answered ${send_time}.",
          "next_action": "Await their response on the license question."
        },
        "notes": "Reply must advance only unsettled logistics or new substance. Never cold-email a replied contact again; continue the live thread."
      }
    },
    {
      "case_id": "T4-deterministic-retry",
      "transaction": "deterministic_retry",
      "given": {
        "daily_outbound_count": 1,
        "attempt_1": "Resend send succeeded (message id mock-2001) but the process died before CRM persistence completed.",
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "name": "Dana Okonkwo",
          "type": "researcher",
          "email": "dana.okonkwo@example.invalid",
          "status": "identified",
          "follow_up_sent": "false",
          "reply_received": "false"
        }
      },
      "action": [
        {
          "attempt": 1,
          "intent": "cold",
          "trigger": "first",
          "idempotency_key": "hermes:c1a00000-0000-4000-8000-000000000001:cold:first"
        },
        {
          "attempt": 2,
          "intent": "cold",
          "trigger": "first",
          "idempotency_key": "hermes:c1a00000-0000-4000-8000-000000000001:cold:first",
          "note": "identical payload, identical key"
        }
      ],
      "expected": {
        "total_sends_across_attempts": 1,
        "message_id": "mock-2001",
        "idempotency_key": "hermes:c1a00000-0000-4000-8000-000000000001:cold:first",
        "cc": [
          "controller.one@example.invalid",
          "controller.two@example.invalid"
        ],
        "crm_after": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "status": "sent",
          "first_outbound_at": "${send_time}",
          "first_outbound_email_id": "mock-2001",
          "last_outbound_at": "${send_time}",
          "last_outbound_email_id": "mock-2001",
          "date_sent": "${send_date}",
          "email_id": "mock-2001",
          "reply_received": "false",
          "follow_up_sent": "false"
        },
        "notes": "Same key + same payload within 24h returns the original send. Exactly one email exists and exactly one complete CRM record exists at the end: no duplicate send, no unlogged send."
      }
    },
    {
      "case_id": "T5-daily-cap-block",
      "transaction": "daily_cap_block",
      "given": {
        "daily_outbound_count": 8,
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "name": "Dana Okonkwo",
          "type": "researcher",
          "email": "dana.okonkwo@example.invalid",
          "status": "sent",
          "reply_received": "false",
          "follow_up_sent": "false",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001"
        }
      },
      "action": {
        "intent": "followup",
        "trigger": "followup-mock-1001",
        "to": "dana.okonkwo@example.invalid"
      },
      "expected": {
        "sends": 0,
        "error": "daily_cap_exceeded",
        "crm_unchanged": true,
        "notes": "At 8 outbound for the day only a live-thread reply that would otherwise sit a full day may still go out, and it still counts. Cold sends and follow-ups block entirely."
      }
    },
    {
      "case_id": "T6-controller-handoff-block",
      "transaction": "controller_handoff_block",
      "given": {
        "daily_outbound_count": 0,
        "thread": {
          "latest_inbound_id": "mock-in-3001",
          "controller_outbound_after_latest_inbound": {
            "from": "controller.one@example.invalid",
            "message_id": "mock-ctrl-4001",
            "note": "A controller replied to the contact on the thread after the latest inbound; no explicit re-entry instruction."
          }
        },
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "name": "Dana Okonkwo",
          "type": "researcher",
          "email": "dana.okonkwo@example.invalid",
          "status": "replied",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001"
        }
      },
      "action": {
        "intent": "reply",
        "trigger": "mock-in-3001",
        "to": "dana.okonkwo@example.invalid"
      },
      "expected": {
        "sends": 0,
        "error": "controller_handoff",
        "crm_after": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "status": "replied",
          "next_action": "Controller is driving this thread; do not reply unless explicitly asked."
        },
        "notes": "Hard stop. Controller replies are detected through RFC Message-ID / In-Reply-To / References threading, with normalized-subject matching only as a labeled fallback. Write-once fields and all other columns unchanged."
      }
    },
    {
      "case_id": "T7-invalid-boolean-format",
      "transaction": "invalid_boolean_format_rejection",
      "given": {
        "crm_row": {
          "id": "c1a00000-0000-4000-8000-000000000001",
          "status": "sent",
          "follow_up_sent": "false",
          "first_outbound_at": "2026-08-15T14:00:00.000Z",
          "first_outbound_email_id": "mock-1001"
        }
      },
      "action": {
        "intent": "crm_update_only",
        "updates": {
          "follow_up_sent": "True"
        }
      },
      "expected": {
        "sends": 0,
        "error": "invalid_follow_up_sent",
        "crm_unchanged": true,
        "notes": "The upsert guard rejects any follow_up_sent value outside the exact lowercase strings 'true'/'false', including Python booleans. The row stays in every triage query instead of silently dropping out."
      }
    }
  ]
}
