# DEB-366: Alert Lifecycle (Agent to understand Escallations)

> **Jira:** [DEB-366](https://divami.atlassian.net/browse/DEB-366)  
> **Source Report:** [20260315.md](../../reports/jira/20260315.md)

- **Status:** Start
- **Assignee:** Maggidi Amulya
- **Priority:** Medium
- **Created:** 2026-03-13T15:57:16.869+0530
- **Updated:** 2026-03-13T16:37:49.774+0530

**Description:**

System should auto understand the escalation from the data snapshot of the most recent refresh and trigger an alert and it should send an email/whatsapp to the user and also udpate the dashboard in the UI.

- Create required tables to store the escalation rules for each data source (it can connect to the above story)
- Defining the Mock data of escalation rules for each datasource and testing them against them


---

## Functional Requirements

| # | Requirement | Description |
|---|---|---|
| FR-01 | Local Red Flag Detection | Individual Data Source Agents (DSAs) must compare fresh deltas against silo-specific escalation rules to identify Red Flags |
| FR-02 | Jira Regression Detection | Must detect delivery regressions, such as a task moving backward from "QA" to "Development" |
| FR-03 | Gmail Sentiment Analysis | Must perform sentiment analysis to flag angry emails from clients or stakeholders |
| FR-04 | Salesforce Deal Monitoring | Must monitor stalled deals, vanishing leads, or high-value lead status changes |
| FR-05 | Asynchronous A2A Communication | Source agents dispatch alert payloads via `message/send`; the Core Agent polls for results via `task/get` |
| FR-06 | Cross-Source Correlation | Core Agent (Chanakya) must update the Org Snapshot (Knowledge Graph) and link disparate signals to identify Compound Insights (e.g. Jira delay + Gmail complaint for the same project) |
| FR-07 | Severity Assessment | Engine must automatically elevate an alert's priority (e.g. P2 → P0) if multiple silos report connected issues for the same entity |
| FR-08 | Personalized Content Slicing | Distribution Engine must determine which stakeholders see which part of correlated news based on their role (e.g. CEO sees revenue risk; Tech Lead sees technical logs) |
| FR-09 | Role Resolution | Engine must resolve generic functional roles (e.g. `"Delivery Lead"`) into specific individuals by querying the Org Snapshot at runtime |
| FR-10 | Multi-Channel Notification | Alerts must be deliverable via Dashboard, Email, Slack, or WhatsApp *(Phase 2 — Phase 1 scope: Email only)* |
| FR-11 | Critical Bypass | For P0/P1 high-severity alerts, the engine must bypass the Dashboard and send immediate proactive notifications via WhatsApp or Email *(Phase 2)* |
| FR-12 | Dynamic Rule Management | Authorised Trainer Persona users must be able to update escalation rules and thresholds via the chat interface |

---

## Non-Functional Requirements

| # | Requirement | Description |
|---|---|---|
| NFR-01 | Proactivity | System must operate on a scheduled heartbeat (cadence), triggering refresh and evaluation cycles independently of user login |
| NFR-02 | Scalability | Architecture must be plug-and-play, allowing new Data Source Agents to be attached with minimal effort; must support logical schema separation for independent database scaling |
| NFR-03 | Security & Governance | RBAC must ensure sensitive information (e.g. financial data) is only distributed to authorised leadership roles |
| NFR-04 | Reliability & Traceability | Every alert must carry evidence in the form of direct links back to source records (Jira tasks, Gmail threads, etc.) for verification |
| NFR-05 | Performance (Low Latency) | Escalation rules must be loaded into memory on application boot to enable fast evaluation against incoming deltas without excessive DB round trips |
| NFR-06 | Token Efficiency | Engine must perform incremental data pulls and use cached world models rather than re-scanning full histories, to minimise LLM costs |
| NFR-07 | Immutability | Rule records must be treated as immutable; updating a rule involves marking the old version as inactive and inserting a new row with an incremented `version_id` |

---

## 1. What This Module Does

This module defines what happens **after** raw data arrives from a scheduled refresh. The Refresh Scheduler and Event Queue are external to this lifecycle — they are responsible for triggering the data pull. This document covers everything from the moment the Data Source Agent receives raw data, through rule evaluation, alert generation, and email dispatch.

> **Boundary:** The Alert Lifecycle starts when the Data Source Agent receives refreshed raw data.

```
Raw Data Received (post-scheduler)
       ↓
Sentiment Enrichment (if applicable)
       ↓
Escalation Rule Evaluation
       ↓
Alert Generated
       ↓
Alert Distribution Engine
       ↓
Email
```

> **Phase 1 scope:** Only Email notifications are supported. WhatsApp, Dashboard, and Slack are planned for Phase 2.

---

## 2. Step-by-Step Alert Lifecycle

> **Entry point:** The Refresh Scheduler has already triggered and raw data has been delivered to the Data Source Agent. The lifecycle begins here.

### Step 1 — Data Source Agent Receives Raw Data

The Data Source Agent (Jira, Salesforce, Gmail, etc.) receives the latest raw JSON from its source API — this is the "delta" from the most recent refresh cycle.

Example raw payload (Gmail):

```json
{
  "thread_id": "TH-9921",
  "sender": "client@tatasteel.com",
  "subject": "Delays are unacceptable",
  "body": "We are extremely unhappy with the current project delays...",
  "received_at": "2026-03-18T09:00:00Z",
  "sentiment_score": null
}
```

### Step 2 — Sentiment Enrichment (Gmail only, where applicable)

For sources that require it (e.g. Gmail), the agent calls its `get_sentiment()` tool before rule evaluation. This is an internal LLM call that returns a score and attaches it to the record before it is written to the DB.

> For Jira and Salesforce, this step is skipped — their escalation conditions are purely structural (field values, status regressions, date thresholds).

The module returns three enrichment fields which are written back to the DB record before rule evaluation:

```json
// After enrichment
{
  "thread_id": "TH-9921",
  "sentiment_score": 2,
  "sentiment_keyword": "angry",
  "tags": ["project", "IBP Portal", "delay", "client-complaint"]
}
```

- `sentiment_score` — integer 1 (extremely negative) to 10 (extremely positive)
- `sentiment_keyword` — single business-friendly tone label (e.g. `furious`, `angry`, `concerned`, `neutral`, `satisfied`, `delighted`)
- `tags` — contextual labels extracted from content: entity type, project name, topic, sender type

> See [dsa-sentiment-enrichment-flows.md](./dsa-sentiment-enrichment-flows.md) for both synchronous and asynchronous enrichment approaches.

### Step 3 — Escalation Rule Evaluation

The agent loads its silo-specific escalation rules from its dedicated rules table (loaded into memory at boot). It evaluates each incoming record against the rules.

Example rule (Jira):

```
trigger_condition: status_regression = True OR ticket_age_hours > 48
recipient_role: Engineering Lead
```

Example rule (Gmail):

```
trigger_condition: sentiment_score <= 3 AND sentiment_keyword = 'angry'
recipient_role: Account Manager
```

When a Red Flag is detected, the agent packages the result as a JSON payload (containing `insights`, `alerts`, and `evidence` links) and dispatches it to the RTDI Agent via the **A2A Protocol**.

### Step 4 — A2A Protocol: Source Agent → RTDI Agent

Every agent operates as an independent microservice. Communication is asynchronous and follows a two-step pattern:

1. **`message/send`** — The source agent dispatches the alert payload and receives an immediate acknowledgement with a unique task ID.
2. **`task/get`** — The RTDI Agent polls the source agent's endpoint every 0.5–1 second using that task ID until the status is `completed`, then fetches the full payload.

```json
// Payload sent by source agent — project_ids only, no data
{
  "source": "jira",
  "project_ids": ["IBP Portal", "EB", "IIRM"]
}
```

### Step 5 — RTDI Agent: Snapshot Comparison & Cross-Source Correlation

Upon receiving the payload, the RTDI Agent (Chanakya):

- **State 1 vs. State 2 comparison** — Compares the new data against the previous Org Snapshot to detect regressions or milestone deviations.
- **Cross-source correlation** — Links signals from multiple silos (e.g. a Gmail complaint + a Jira regression for the same project) into a compound insight.
- **Compound insight synthesis** — Combines correlated signals into a single unified alert message.

### Step 6 — Alert Object Created

Once the RTDI Agent synthesises the correlated insight, it creates an alert object and stores it in the `alerts` table:

```json
{
  "alert_id": "ALT1001",
  "type": "PROJECT_ESCALATION",
  "message": "Project IBP Portal delayed by 5 days. Jira regression detected. Negative client sentiment on Gmail.",
  "recipient_role": "Delivery Lead",
  "status": "OPEN"
}
```

### Step 7 — Alert Distribution Engine

The **User Aware Distribution Engine**:

1. Resolves `recipient_role` (e.g., `"Delivery Lead"`) into a specific individual by querying the Org Snapshot at runtime.
2. Dispatches the alert via **Email** (Phase 1 only).
3. Logs the delivery status in `alert_notifications`.

**Phase 1 channel:** Email only.

> WhatsApp, Dashboard, and Slack are out of scope for Phase 1 and will be added in Phase 2.

### Step 8 — User Notification

Example notification:

```
Email
To: rahul@company.com
Subject: Escalation Alert — Project IBP Portal

Project IBP Portal is delayed by 5 days.
Jira regression detected. Negative client sentiment on Gmail.

Delivery Lead: Rahul
Customer: Tata Steel
```

---

## 3 Low-Level Architecture

> The Refresh Scheduler and Event Queue sit outside this diagram. This architecture shows the alert lifecycle starting from the point raw data is received by a Data Source Agent.

```mermaid
flowchart TD
    A([Raw Data Received\nfrom Scheduler Trigger]) --> B

    subgraph DSA [Data Source Agent]
        B[Receive raw JSON delta]
        B --> C{Requires sentiment\nenrichment?}
        C -->|Yes - Gmail| D["get_sentiment(text)\nLLM call"]
        D --> E[Attach sentiment_score]
        E --> F[Store enriched record in DB]
        C -->|No - Jira / Salesforce| F
        F --> G[Load escalation rules\nfrom memory]
        G --> H{Rule triggered?}
        H -->|No| I([End - no alert])
        H -->|Yes| J[Build payload\ninsights + alerts + evidence]
    end

    J -->|A2A: message/send| K[RTDI Agent - Chanakya]
    K -->|A2A: task/get poll| J

    subgraph RTDI [RTDI Agent]
        K --> L[State 1 vs State 2 comparison]
        L --> M[Cross-source correlation]
        M --> N[Compound insight synthesis]
        N --> O[Create alert object\nstore in alerts table]
    end

    O --> P[Distribution Engine]

    subgraph DIST [Distribution Engine]
        P --> Q[Resolve recipient_role\nvia Org Snapshot]
        Q --> R[Compose role-appropriate\nemail summary]
        R --> S[Send Email]
        S --> T[Log in alert_notifications]
    end
```

---

## 4. Escalation Detection Algorithm (Core Logic)

**At each Data Source Agent (runs per source):**

```python
rules = load_source_rules(source="jira")   # loaded from jira_escalation_rules on boot

for entity in fresh_data:
    for rule in rules:
        if evaluate(entity, rule.trigger_condition):
            payload = build_payload(entity, rule)  # insights + alerts + evidence
            task_id = message_send(rtdi_agent, payload)  # A2A: send & get ack
            poll_until_complete(task_id)              # task/get every 0.5–1s
```

**At the RTDI Agent (cross-source correlation):**

```python
prev_snapshot = get_snapshot(state=1)
curr_snapshot = get_snapshot(state=2)

for signal in incoming_signals:  # from all source agents
    regression = compare(prev_snapshot, curr_snapshot, signal.entity_id)
    related    = correlate_across_sources(signal, all_signals)  # e.g. Gmail + Jira
    insight    = synthesise_insight(regression, related)
    create_alert(signal.entity_id, insight)
```

---

## 5. Database Schema

### Escalation Rules — Per-Source Tables

Each data source has its own dedicated escalation rules table. Rules are **not centralised** — what constitutes a Red Flag is domain-specific (e.g., a Jira status regression is fundamentally different from a Gmail sentiment score). Rules are stored as JSON/text in PostgreSQL, allowing the **Trainer Persona** to update them dynamically via chat.

> **Immutable update policy:** Rules are never mutated. When a trainer updates a rule, the old row is marked `active_flag = FALSE` and a new row is inserted with an incremented `version_id`.

> **`recipient_role`:** Maps to a functional role (e.g., `"Delivery Lead"`), not a hard-coded user. The Distribution Engine resolves the actual person at dispatch time by querying the Org Snapshot.

**Shared schema (applied per source as `<source>_escalation_rules`):**

```sql
-- Example: jira_escalation_rules
-- Also: salesforce_escalation_rules, gmail_escalation_rules, etc.

CREATE TABLE jira_escalation_rules (
    rule_id           SERIAL PRIMARY KEY,
    entity_type       VARCHAR(50),   -- e.g. 'Bug', 'Task', 'Project'
    trigger_condition TEXT,          -- e.g. 'status_regression = True OR ticket_age_hours > 48'
    action_type       VARCHAR(50)    DEFAULT 'email',  -- Phase 1: email only
    recipient_role    VARCHAR(100),  -- e.g. 'Engineering Lead'
    version_id        INT            DEFAULT 1,
    active_flag       BOOLEAN        DEFAULT TRUE,
    rule_metadata     JSONB,         -- reference to Runbook / SOP
    created_at        TIMESTAMP      DEFAULT NOW()
);
```

> **Phase 1:** `action_type` is always `email`. WhatsApp, Dashboard, and Slack will be added in Phase 2.

Example data (`gmail_escalation_rules`):

| rule_id | source_id | entity_type | trigger_condition | action_type | recipient_role |
|---|---|---|---|---|---|
| 1 | gmail | Thread | `sentiment_score < 0.3` | email | Account Manager |

---

### Table — `alerts`

```sql
CREATE TABLE alerts (
    alert_id   SERIAL PRIMARY KEY,
    source_id  VARCHAR(50),   -- which data source triggered this
    rule_id    INT,           -- references the relevant <source>_escalation_rules table
    entity_id  VARCHAR(50),
    message    TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    status     VARCHAR(20) DEFAULT 'OPEN'
);
```

---

### Table — `alert_notifications`

```sql
CREATE TABLE alert_notifications (
    notification_id SERIAL PRIMARY KEY,
    alert_id        INT REFERENCES alerts(alert_id),
    user_id         VARCHAR(50),
    channel         VARCHAR(20) DEFAULT 'email',  -- Phase 1: email only
    status          VARCHAR(20),
    sent_at         TIMESTAMP
);
```

---

## 6. Mock Escalation Rules Data

### `jira_escalation_rules`

| # | entity_type | trigger_condition | action_type | recipient_role | rule_metadata |
|---|---|---|---|---|---|
| 1 | Task | `ticket_age_hours > 48 AND priority = 'High'` | email | Delivery Lead | Runbook: SOP-JIRA-01 |
| 2 | Bug | `status_regression = True` | email | Engineering Lead | Runbook: SOP-JIRA-02 |
| 3 | Bug | `reopen_count > 3` | email | Engineering Lead | Runbook: SOP-JIRA-03 |
| 4 | Sprint | `delay_days > 3` | email | Delivery Lead | Runbook: SOP-JIRA-04 |
| 5 | Sprint | `burn_rate > planned_budget` | email | Project Manager | Runbook: SOP-JIRA-05 |
| 6 | Task | `unassigned_hours > 24 AND priority = 'Critical'` | email | Engineering Lead | Runbook: SOP-JIRA-06 |

---

### `salesforce_escalation_rules`

| # | entity_type | trigger_condition | action_type | recipient_role | rule_metadata |
|---|---|---|---|---|---|
| 1 | Deal | `deal_stuck_days > 30` | email | Account Executive | Runbook: SOP-SF-01 |
| 2 | Lead | `lead_value > 50000 AND activity_days = 0` | email | VP of Sales | Runbook: SOP-SF-02 |
| 3 | Deal | `deal_value > 100000 AND stage_regression = True` | email | VP of Sales | Runbook: SOP-SF-03 |
| 4 | Lead | `follow_up_overdue_days > 5` | email | Account Executive | Runbook: SOP-SF-04 |
| 5 | Opportunity | `close_date_passed = True AND status = 'Open'` | email | Account Executive | Runbook: SOP-SF-05 |
| 6 | Account | `churn_risk_score > 0.7` | email | Customer Success Lead | Runbook: SOP-SF-06 |

---

### `gmail_escalation_rules`

| # | entity_type | trigger_condition | action_type | recipient_role | rule_metadata |
|---|---|---|---|---|---|
| 1 | Bullet | `sentiment_score <= 2` | email | CEO | Runbook: SOP-GM-01 — Extreme negative sentiment (P0); score 1–2 on 1–10 scale signals immediate escalation |
| 2 | Bullet | `sentiment_score <= 3 AND sentiment_keyword = 'angry'` | email | Delivery Manager | Runbook: SOP-GM-02 — Angry sentiment keyword combined with low score |
| 3 | Bullet | `sentiment_score <= 4 AND sender LIKE '%@tatasteel.com'` | email | CEO | Runbook: SOP-GM-03 — VIP client domain; escalate earlier than standard threshold |
| 4 | Bullet | `'client-complaint' = ANY(tags)` | email | Delivery Manager | Runbook: SOP-GM-04 — Complaint tag explicitly set during LLM enrichment; more reliable than keyword matching |
| 5 | Bullet | `'delay' = ANY(tags) AND sentiment_score <= 4` | email | Delivery Manager | Runbook: SOP-GM-05 — Delivery risk signal: delay context + negative sentiment combined |
| 6 | Bullet | `'escalation' = ANY(tags) OR 'legal' = ANY(tags)` | email | Delivery Head | Runbook: SOP-GM-08 — High-risk language (escalation or legal); critical bypass in Phase 2 |
| 7 | Bullet | `sentiment_score <= 4 AND array_length(tags, 1) >= 3` | email |  Lead | Runbook: SOP-GM-10 — Multi-signal: rich tag context + negative sentiment; likely cross-source correlation candidate |
---

## 7. Example Alert Lifecycle

> Starts after the scheduler has triggered and raw data has been delivered to the Data Source Agent.

```
Raw data received by Data Source Agent
    ↓
Sentiment enrichment (Gmail only) — get_sentiment() LLM call
    ↓
Record stored in DB with sentiment_score populated
    ↓
Escalation rules evaluated against fresh data
    ↓
Red Flag detected → payload built (insights + alerts + evidence)
    ↓
A2A: message/send → RTDI Agent (ack + task ID received)
    ↓
A2A: task/get polling (every 0.5–1s) → payload delivered
    ↓
RTDI Agent: State 1 vs. State 2 comparison + cross-source correlation
    ↓
Compound insight synthesised
    ↓
Alert object created and stored in `alerts` table
    ↓
Distribution Engine resolves recipient_role → person via Org Snapshot
    ↓
Role-appropriate email composed and dispatched
    ↓
Logged in `alert_notifications` table
```

---

## 08. Implementation Tasks

### Phase 1 — Database & Schema Setup

| Task | Description | Estimate |
|---|---|---|
| **TASK-01** Per-Source Escalation Rule Tables | Create migrations for `jira_escalation_rules`, `salesforce_escalation_rules`, and `gmail_escalation_rules` with columns for `trigger_condition`, `action_type` (email), `recipient_role`, `version_id`, and `active_flag` | 16h |
| **TASK-02** Alerts & Notifications Tables | Create `alerts` table to store synthesised correlated insights and `alert_notifications` table to track delivery status per channel | 8h |
| **TASK-03** Immutable Versioning & Seeding | Implement soft-deactivation logic (`active_flag = FALSE` on update) and seed tables with realistic mock data | 16h |

### Phase 2 — Sentiment Enrichment Module

| Task | Description | Estimate |
|---|---|---|
| **TASK-23** Sentiment Enrichment Schema Migration | Add `sentiment_score INT`, `sentiment_keyword VARCHAR(50)`, `sentiment_tags JSONB`, and `sentiment_processed BOOLEAN` columns to the `gmail_messages` table | 4h |
| **TASK-24** LLM Sentiment Module | Implement `get_sentiment(id, info)`: build the structured LLM prompt, parse and validate JSON response, return `(id, sentiment_score, sentiment_keyword, tags)` | 16h |
| **TASK-25** Background Enrichment Processor | Build the async background worker that queries `WHERE sentiment_processed = FALSE`, calls `get_sentiment()` per record, and updates the DB with all three enrichment fields | 12h |
| **TASK-26** Escalation Rule Guard for NULL | Update rule evaluator to skip Gmail records where `sentiment_processed = FALSE`, preventing false-positive triggers during the enrichment window | 4h |

### Phase 3 — Per-Source Rule Engine

| Task | Description | Estimate |
|---|---|---|
| **TASK-04** Rule Loader Service | Load all `active_flag = TRUE` rules into memory on boot to avoid high-frequency DB round trips | 12h |
| **TASK-05** Local Threshold & Regression Evaluator | Compare fresh deltas against stored rules; includes specialised logic for Jira status regressions and Gmail sentiment scoring (`< 0.3`) | 24h |
| **TASK-06** Local Insight Synthesiser | Package detected Red Flags into a standardised JSON payload containing `insights`, `alerts`, and `evidence` links | 8h |

### Phase 4 — A2A Protocol Implementation

| Task | Description | Estimate |
|---|---|---|
| **TASK-07** Asynchronous Dispatcher (`message/send`) | Implement the sender side of the A2A Protocol to dispatch alert packets to the RTDI Agent and receive an acknowledgement task ID | 16h |
| **TASK-08** Polling Service (`task/get`) | Build the polling mechanism to check task status every 0.5–1 second until `completed` and fetch the full payload | 16h |
| **TASK-09** Idempotency & Metadata Handling | Maintain packet IDs across agents to prevent duplicate processing | 8h |

### Phase 5 — RTDI Correlation Engine

| Task | Description | Estimate |
|---|---|---|
| **TASK-10** State 1 vs. State 2 Snapshot Comparator | Fetch the previous snapshot (State 1) and compare it with fresh data (State 2) to detect business-level regressions and deviations | 24h |
| **TASK-11** Cross-Source Signal Correlator | Link disparate signals across silos (e.g. Jira task delay + Gmail negative sentiment for the same project) into compound insights | 24h |
| **TASK-12** Compound Insight Synthesis | Combine signals from multiple data sources on the same entity into a unified alert message | 8h |

### Phase 6 — User Aware Distribution Engine

| Task | Description | Estimate |
|---|---|---|
| **TASK-13** Role Resolver Service | Query the Org Snapshot at runtime to resolve `recipient_role` (e.g. `"Delivery Lead"`) into the specific person currently assigned to that role | 16h |
| **TASK-14** Role-Based Email Dispatcher | Resolve recipient role to a person, compose a role-appropriate email summary, and send via Email (Phase 1 only) | 16h |

### Phase 7 — Trainer Persona: Rule Management API

| Task | Description | Estimate |
|---|---|---|
| **TASK-18** Rule Update Interface | Expose endpoints for the Trainer Persona to update business logic and thresholds via the chat interface | 16h |
| **TASK-19** Version History & Audit | Build views to track the full history of rule changes for auditing purposes | 12h |

### Phase 8 — Testing & Validation

| Task | Description | Estimate |
|---|---|---|
| **TASK-20** Unit Testing | Write tests for `ThresholdEvaluator` and `RegressionDetector` using mock snapshots | 24h |
| **TASK-21** E2E Pipeline Validation | Test the full flow: Scheduler → Refresh → Correlation → Alert → Notification | 24h |
| **TASK-22** Load & Stress Testing | Ensure the heartbeat mechanism stays performant with 20+ connected data sources | 12h |

---

**Total Estimate: ~256h (~17 days)**

---

## 09. Test Cases

### TC-01: Escalation Rule Table Creation — Schema Validation
| Step | Action | Expected Result |
|---|---|---|
| 1 | Run migration `20260324_001.sql` against a fresh `eb` schema | All three tables created: `eb.jira_escalation_rules`, `eb.salesforce_escalation_rules`, `eb.gmail_escalation_rules` |
| 2 | Check column presence on `eb.jira_escalation_rules` | Columns `rule_id`, `entity_type`, `trigger_condition`, `action_type`, `recipient_role`, `version_id`, `active_flag`, `rule_metadata`, `created_at` all present |
| 3 | Run migration a second time (idempotency check) | No error; no duplicate tables created |
| 4 | Run rollback script | All three tables removed; `eb` schema clean |

---

### TC-02: Escalation Rule Seeding — Mock Data Integrity
| Step | Action | Expected Result |
|---|---|---|
| 1 | Run seed inserts from `20260324_001.sql` | 6 rows in `eb.jira_escalation_rules`, 6 in `eb.salesforce_escalation_rules`, 8+ in `eb.gmail_escalation_rules` |
| 2 | `SELECT * FROM eb.gmail_escalation_rules WHERE active_flag = TRUE` | All seeded rows returned with `active_flag = TRUE` and `version_id = 1` |
| 3 | Verify `rule_metadata` is valid JSONB on all rows | No parse errors; `rule_metadata->>'runbook'` returns a non-null value for every row |

---

### TC-03: Immutable Rule Update — Soft Deactivation

> Covers NFR-07 (Immutability) and FR-12 (Dynamic Rule Management).

| Step | Action | Expected Result |
|---|---|---|
| 1 | Fetch rule `rule_id = 1` from `eb.jira_escalation_rules` | Row returned with `active_flag = TRUE`, `version_id = 1` |
| 2 | `UPDATE eb.jira_escalation_rules SET active_flag = FALSE WHERE rule_id = 1` | Row now has `active_flag = FALSE`; data not deleted |
| 3 | `INSERT INTO eb.jira_escalation_rules (entity_type, trigger_condition, recipient_role, version_id) VALUES ('Task', 'ticket_age_hours > 24 AND priority = ''Critical''', 'Engineering Lead', 2)` | New row inserted with `version_id = 2`, `active_flag = TRUE` |
| 4 | `SELECT * FROM eb.jira_escalation_rules WHERE entity_type = 'Task' ORDER BY version_id` | Returns both rows — old (inactive v1) and new (active v2); full history preserved |
| 5 | Re-run `SELECT * FROM eb.jira_escalation_rules WHERE active_flag = TRUE` | Only v2 row returned for that rule |

---

### TC-04: Rule Loader — In-Memory Boot

> Covers NFR-05 (Performance / Low Latency). Rules must load at boot, not per-request.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Start the Data Source Agent process | Agent logs confirm rules loaded into memory from `eb.gmail_escalation_rules WHERE active_flag = TRUE` |
| 2 | Update a rule in DB (`active_flag = FALSE` on one row; insert new row) without restarting the agent | Agent still evaluates using the previously loaded rules (stale — expected until refresh) |
| 3 | Trigger a rule reload (via admin signal or TTL expiry) | Agent re-reads DB; updated rule set reflected in memory |
| 4 | Simulate 1000 consecutive email bullet evaluations | Zero DB round trips observed during evaluation; all evaluation runs against in-memory rules |

---

### TC-05: Gmail Sentiment Enrichment — `get_sentiment()` Output

> Covers FR-03 (Gmail Sentiment Analysis), TASK-24.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Call `get_sentiment(id, info)` with an email containing strongly negative client language ("This is completely unacceptable, we are considering legal action") | Returns `sentiment_score <= 2`, `sentiment_keyword = 'furious'`, `tags` includes `['client-complaint', 'legal']` |
| 2 | Call `get_sentiment()` with a routine status-update email | Returns `sentiment_score >= 7`, `sentiment_keyword = 'neutral'` or `'satisfied'`, no escalation tags |
| 3 | Call `get_sentiment()` with a malformed LLM response (missing fields) | Parser raises validation error; record is not written to DB; `sentiment_processed` remains `FALSE` |
| 4 | After successful enrichment, query the DB record | `sentiment_score`, `sentiment_keyword`, and `tags` all populated; `sentiment_processed = TRUE` |

---

### TC-06: Escalation Rule Evaluation — Gmail Rule Matching

> Covers FR-01 (Local Red Flag Detection), FR-03 (Gmail Sentiment Analysis).

| Step | Action | Expected Result |
|---|---|---|
| 1 | Insert bullet with `sentiment_score = 1` into `eb.email_bullets` | Evaluator matches rule SOP-GM-01 (`sentiment_score <= 2`) |
| 2 | Insert bullet with `sentiment_score = 3`, `sentiment_keyword = 'angry'` | Evaluator matches rule SOP-GM-02; `recipient_role = 'Delivery Manager'` |
| 3 | Insert bullet with `sender = 'cto@tatasteel.com'`, `sentiment_score = 4` | Evaluator matches rule SOP-GM-03 (VIP domain) |
| 4 | Insert bullet with `tags = ['client-complaint', 'delay', 'legal']` | Evaluator matches SOP-GM-04 (complaint tag), SOP-GM-05 (delay tag + score), SOP-GM-08 (legal tag) — multiple rules fire |
| 5 | Insert bullet with `sentiment_score = 8` (positive), no alarm tags | No rules fire; no Red Flag generated |
| 6 | Insert bullet where `sentiment_processed = FALSE` | Evaluator skips this record (TASK-26 guard); no false positive |

---

### TC-07: Escalation Rule Evaluation — Jira Regression Detection

> Covers FR-02 (Jira Regression Detection).

| Step | Action | Expected Result |
|---|---|---|
| 1 | Simulate a Jira task where `status_regression = True` (e.g., moved from QA → Development) | Rule SOP-JIRA-02 fires; Red Flag generated with `recipient_role = 'Engineering Lead'` |
| 2 | Simulate a Bug with `reopen_count = 4` | Rule SOP-JIRA-03 fires; `recipient_role = 'Engineering Lead'` |
| 3 | Simulate a Sprint with `delay_days = 5` | Rule SOP-JIRA-04 fires; `recipient_role = 'Delivery Lead'` |
| 4 | Simulate a Task with `ticket_age_hours = 30`, `priority = 'Medium'` | No rule fires (priority is not 'High') |
| 5 | Simulate a Task with `ticket_age_hours = 49`, `priority = 'High'` | Rule SOP-JIRA-01 fires |

---

### TC-08: Escalation Rule Evaluation — Salesforce Deal Monitoring

> Covers FR-04 (Salesforce Deal Monitoring).

| Step | Action | Expected Result |
|---|---|---|
| 1 | Simulate a Deal with `deal_stuck_days = 35` | Rule SOP-SF-01 fires; `recipient_role = 'Account Executive'` |
| 2 | Simulate a Lead with `lead_value = 75000`, `activity_days = 0` | Rule SOP-SF-02 fires; `recipient_role = 'VP of Sales'` |
| 3 | Simulate an Account with `churn_risk_score = 0.85` | Rule SOP-SF-06 fires; `recipient_role = 'Customer Success Lead'` |
| 4 | Simulate a Lead with `lead_value = 30000`, `activity_days = 0` | No rule fires (lead value below threshold) |

---

### TC-09: A2A Protocol — `message/send` Dispatch

> Covers FR-05 (Asynchronous A2A Communication), TASK-07.

| Step | Action | Expected Result |
|---|---|---|
| 1 | DSA detects a Red Flag and calls `message/send` with `{ "source": "gmail", "project_ids": ["EB", "IIRM"] }` | RTDI Agent responds with `202 Accepted` and a unique `task_id` |
| 2 | Inspect the acknowledged `task_id` format | Non-empty UUID; unique across calls |
| 3 | Call `message/send` with an empty `project_ids` list | `400 Bad Request` — payload validation fails |
| 4 | Call `message/send` twice with identical content | Both calls accepted; separate `task_id`s returned (idempotency key required to deduplicate — TASK-09) |

---

### TC-10: A2A Protocol — `task/get` Polling and Completion

> Covers FR-05 (Asynchronous A2A Communication), TASK-08.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Send `message/send`; receive `task_id` | `task_id` received |
| 2 | Immediately poll `task/get?task_id={id}` | Status is `pending` or `processing` |
| 3 | Poll every 0.5 seconds until status changes | Within expected window, status transitions to `completed` |
| 4 | Fetch full payload from completed task | Payload contains processed result from RTDI Agent |
| 5 | Poll a non-existent `task_id` | `404 Not Found` |

---

### TC-11: RTDI Agent — State 1 vs. State 2 Snapshot Comparison

> Covers FR-06 (Cross-Source Correlation), FR-07 (Severity Assessment), TASK-10.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Seed `eb.org_project_snapshot` with previous state (State 1) for project `EB` | Snapshot row exists with `bullet_ids`, `avg_sentiment_score = 7.2` |
| 2 | Notify RTDI Agent of new data for project `EB` (State 2 includes 3 new negative bullets) | RTDI Agent detects `new_bullet_ids`; `avg_sentiment_score` drops |
| 3 | Verify `resolved_bullet_ids` are populated for bullets no longer in the active window | Resolved bullets recorded in snapshot; not deleted from `eb.email_bullets` |
| 4 | Verify `dominant_type` updates from `information` → `alert` when alert-type bullets are new | `dominant_type = 'alert'` in updated snapshot |

---

### TC-12: Cross-Source Correlation — Compound Insight

> Covers FR-06 (Cross-Source Correlation), FR-07 (Severity Assessment), TASK-11.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Gmail DSA notifies RTDI of `project_ids: ["EB"]` with 2 negative bullets (`sentiment_score <= 3`) | Gmail signal logged |
| 2 | Jira DSA independently notifies RTDI of `project_ids: ["EB"]` with a `status_regression` task | Jira signal logged |
| 3 | RTDI correlates both signals for the same `project_id = "EB"` | Compound insight synthesised: "Sprint delay + negative client sentiment for project EB" |
| 4 | Verify alert priority is elevated | Alert created with `priority = 'P0'`; higher than either individual signal's priority |
| 5 | Verify `alerts.source_links` contains references to both the Gmail bullet and the Jira task | Evidence links present in the alert object |

---

### TC-13: Alert Object Creation and Persistence

> Covers Step 6 of the alert lifecycle.

| Step | Action | Expected Result |
|---|---|---|
| 1 | RTDI Agent completes compound insight synthesis for project `EB` | `INSERT INTO alerts` with `alert_id`, `project_id = 'EB'`, `status = 'OPEN'`, `priority`, `source_links` |
| 2 | Query `SELECT * FROM alerts WHERE project_id = 'EB'` | Alert row returned with all fields populated |
| 3 | Verify `source_links` JSONB contains at least one Gmail thread link and one Jira task link | Links present; traceable to source records (NFR-04) |
| 4 | Attempt to read a non-existent `alert_id` | `404 Not Found` |

---

### TC-14: Distribution Engine — Role Resolution

> Covers FR-08 (Personalised Content Slicing), FR-09 (Role Resolution), TASK-13.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Seed `org_info.org_people` with a person assigned `role = 'Delivery Lead'` for project `EB` | Person record exists |
| 2 | Distribution Engine receives alert with `recipient_role = 'Delivery Lead'` for project `EB` | Engine queries `org_info.org_people` + `org_info.org_project_assignments`; resolves to the seeded person |
| 3 | Seed org structure with no one assigned as `Delivery Lead` for project `EB` | Distribution Engine logs a resolution failure; alert is not silently dropped — error is raised |
| 4 | Seed two people with the same `role = 'Delivery Lead'` for the same project | Engine resolves to the primary assignee (first match or most recent `assigned_at`); no duplicate dispatch |

---

### TC-15: Email Notification Dispatch

> Covers FR-10 (Multi-Channel Notification — Phase 1: Email only), TASK-14.

| Step | Action | Expected Result |
|---|---|---|
| 1 | Trigger full pipeline for project `EB` with a P0 Gmail escalation | Email composed and dispatched to the resolved `Delivery Lead` |
| 2 | Verify email subject contains project name | Subject matches pattern `"Escalation Alert — Project EB"` |
| 3 | Verify `alert_notifications` table is updated | Row inserted with `alert_id`, `channel = 'email'`, `recipient_email`, `sent_at` timestamp |
| 4 | Attempt to dispatch to an invalid email address | Delivery fails gracefully; `alert_notifications.status = 'failed'`; no exception propagates |

---

### TC-16: Alerts Table — Immutability and Logging

> Covers NFR-04 (Reliability & Traceability).

| Step | Action | Expected Result |
|---|---|---|
| 1 | Create an alert; then attempt `DELETE FROM alerts WHERE alert_id = ?` via API | `405 Method Not Allowed` — alerts are immutable |
| 2 | Attempt `UPDATE alerts SET status = 'CLOSED'` via an unauthorised role | `403 Forbidden` — only authorised roles can update alert status |
| 3 | Verify every alert has `source_links` JSON with at least one entry | No `NULL` source_links; every alert is traceable to evidence (NFR-04) |

---
