# PR/Issue Graph Linking - Implementation Plan

**Status:** Phase 2 Completion - Remaining Work
**Priority:** High
**Estimated Effort:** 2-3 hours
**Dependencies:** None (can be done independently)

---

## 🎯 Objective

Enable graph queries like:
- "What PRs touched this function?"
- "What files were changed by PR #123?"
- "Show me all commits related to issue #456"

## 📊 Current State

**What Works:**
- ✅ Webhook endpoints for GitHub/GitLab push events (`packages/clew-api/src/routes/webhooks.py`)
- ✅ File change extraction from commits
- ✅ Event deduplication with Redis
- ✅ Graph has Repository → File → Function structure

**What's Missing:**
- ❌ No PullRequest or Issue nodes in Neo4j graph
- ❌ No PR/Issue webhook handlers (only push events)
- ❌ No commit message parsing for PR/Issue references
- ❌ No PR → File → Function relationships

**Current Graph Schema:**
```
Repository (1 node)
  └─ File (160 nodes)
      └─ Module (132 nodes)
      └─ Function (750 nodes)
          └─ CodeBlock (593 nodes)

Relationships:
- CONTAINS (Repository → File)
- IMPORTS (Module → Module)
- DEFINES (Function → CodeBlock)
- CALLS (Function → Function)
```

---

## 📋 Implementation Tasks

### Task 1: Add PR/Issue Webhook Handlers

**File:** `packages/clew-api/src/routes/webhooks.py`

**Add Methods:**

1. `GitHubWebhookProcessor.extract_pr_event(payload)` - Extract PR metadata
   ```python
   Returns:
   {
       "event_type": "pull_request",
       "action": "opened|closed|merged",
       "pr_number": 123,
       "pr_title": "Fix authentication bug",
       "pr_url": "https://github.com/...",
       "pr_state": "open|closed|merged",
       "author": "username",
       "base_branch": "main",
       "head_branch": "feature/fix",
       "commits": ["sha1", "sha2", ...],
       "changed_files": ["file1.py", "file2.py", ...],
       "repo_id": "owner/repo"
   }
   ```

2. `GitLabWebhookProcessor.extract_pr_event(payload)` - Same for GitLab (uses "merge_request")

3. Add route handlers:
   ```python
   @router.post("/github/pull_request")
   async def github_pr_webhook(...)

   @router.post("/gitlab/merge_request")
   async def gitlab_mr_webhook(...)
   ```

**GitHub PR Webhook Payload Reference:**
- Event: `pull_request`
- Actions: `opened`, `closed`, `synchronize`, `reopened`
- Payload: `payload["pull_request"]` contains metadata

**GitLab MR Webhook Payload Reference:**
- Event: `merge_request`
- Object kind: `merge_request`
- Payload: `payload["object_attributes"]` contains metadata

### Task 2: Create PR/Issue Graph Nodes

**File:** Create `packages/clew-ingestion/src/graph_pr_linker.py`

```python
from neo4j import GraphDatabase

class PRGraphLinker:
    """Links PRs/Issues to the code graph."""

    def __init__(self, neo4j_driver):
        self.driver = neo4j_driver

    async def create_pr_node(self, pr_data: dict) -> str:
        """
        Create PR node and link to repository.

        Returns PR node ID.
        """
        # Create PR node
        # Link to Repository
        # Link to Files (changed_files)
        # Return node ID

    async def link_pr_to_files(self, pr_id: str, file_paths: list[str]):
        """Create MODIFIES relationships between PR and File nodes."""

    async def parse_commit_references(self, commit_message: str) -> list[int]:
        """
        Extract PR/Issue references from commit message.

        Patterns:
        - #123
        - fixes #456
        - closes #789
        - resolves #012
        """
```

**Neo4j Schema Extension:**

```cypher
// New node types
(:PullRequest {
    number: int,
    title: string,
    url: string,
    state: string,
    author: string,
    created_at: datetime,
    merged_at: datetime,
    base_branch: string,
    head_branch: string
})

(:Issue {
    number: int,
    title: string,
    url: string,
    state: string,
    author: string,
    created_at: datetime,
    closed_at: datetime
})

// New relationships
(PullRequest)-[:MODIFIES]->(File)
(PullRequest)-[:REFERENCES]->(Issue)
(Commit)-[:BELONGS_TO]->(PullRequest)
```

### Task 3: Wire into Webhook Flow

**File:** `packages/clew-api/src/routes/webhooks.py`

Update `dispatch_sync_job()`:

```python
async def dispatch_sync_job(changes: dict[str, Any]) -> str:
    """
    Dispatch incremental sync job.

    Now handles both push events and PR/Issue events.
    """
    # Existing push event handling...

    # NEW: Handle PR/Issue events
    if changes.get("event_type") == "pull_request":
        from src.graph_pr_linker import PRGraphLinker
        linker = PRGraphLinker(neo4j_driver)
        await linker.create_pr_node(changes)
        await linker.link_pr_to_files(
            changes["pr_number"],
            changes["changed_files"]
        )
```

### Task 4: Add Tests

**File:** Create `packages/clew-api/tests/test_pr_webhooks.py`

```python
def test_github_pr_opened_event():
    """Test GitHub PR opened event creates PR node."""

def test_gitlab_mr_merged_event():
    """Test GitLab MR merged event creates PR node."""

def test_pr_links_to_changed_files():
    """Test PR → File relationships created."""

def test_commit_message_parsing():
    """Test extraction of #123 references."""
```

### Task 5: Add Graph Queries

**File:** `packages/clew-api/src/routes/graph.py`

Add new endpoints:

```python
@router.get("/file/{file_path}/pull_requests")
async def get_file_pull_requests(file_path: str):
    """Get all PRs that modified this file."""

@router.get("/pull_request/{pr_number}/impact")
async def get_pr_impact(pr_number: int):
    """Get all files and functions affected by this PR."""
```

---

## ✅ Acceptance Criteria

1. Can receive GitHub PR webhook events (opened, closed, merged)
2. Can receive GitLab MR webhook events
3. PR nodes created in Neo4j with metadata
4. PR → File relationships established
5. Can query: `MATCH (pr:PullRequest)-[:MODIFIES]->(f:File) WHERE pr.number = 123 RETURN f`
6. Can query: `MATCH (f:File)<-[:MODIFIES]-(pr:PullRequest) WHERE f.path = "x.py" RETURN pr`
7. Commit message parsing extracts #123 references
8. Tests pass for webhook processing and graph creation

---

## 🧪 Testing Plan

**Manual Testing:**

1. Start Neo4j and API server
2. Send test webhook:
   ```bash
   curl -X POST http://localhost:8001/v1/webhooks/github/pull_request \
     -H "Content-Type: application/json" \
     -d @tests/fixtures/github_pr_opened.json
   ```
3. Verify in Neo4j Browser:
   ```cypher
   MATCH (pr:PullRequest) RETURN pr
   MATCH (pr:PullRequest)-[:MODIFIES]->(f:File) RETURN pr, f
   ```

**Integration Testing:**
- Run existing integration tests
- Add PR-specific tests
- Verify no regressions in push event handling

---

## 📚 Resources

**GitHub Webhook Docs:**
- https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request

**GitLab Webhook Docs:**
- https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#merge-request-events

**Neo4j Cypher Patterns:**
- Relationship creation: `CREATE (a)-[:REL]->(b)`
- Pattern matching: `MATCH (a)-[r:REL]->(b)`

**Current Webhook Implementation:**
- `packages/clew-api/src/routes/webhooks.py` (lines 234-273 show dispatch pattern)
- `packages/clew-ingestion/src/graph.py` (existing Neo4j interface)

---

## 🚀 Getting Started

1. Read current webhook implementation: `packages/clew-api/src/routes/webhooks.py`
2. Review graph store interface: `packages/clew-ingestion/src/graph.py`
3. Create test fixture: `tests/fixtures/github_pr_opened.json` (sample PR payload)
4. Implement `extract_pr_event()` method
5. Test with curl → verify in Neo4j Browser
6. Iterate on graph schema and queries

---

## ⚠️ Known Limitations

- Only handles PR open/close/merge (not review comments, approvals)
- No Issue node creation yet (PRs only for Phase 2)
- No commit → PR linking (just PR → File → Function)
- Historical PRs not backfilled (only new webhooks)

These can be addressed in Phase 3 or future iterations.
