# PR Tracking Capabilities and Developer Alias Mapping — Gap Analysis

**Date:** 2026-02-25
**Project:** gitflow-analytics
**Scope:** PR lifecycle tracking, per-developer PR attribution, and developer identity resolution

---

## Executive Summary

The system has solid foundations for PR data collection and identity resolution but has significant
gaps in three areas:

1. **PRs closed without merge (rejected)** are explicitly filtered out of tracking — zero coverage.
2. **PR authorship is stored as a raw GitHub login string** and is never resolved through the
   identity resolver, so GitHub username-to-git-author mapping is broken in reports.
3. **Per-developer PR counts** (authored, reviewed, commented) are not computed or surfaced — only
   aggregate PR totals appear in reports.
4. **Co-author trailer parsing** strips co-authors out of commit messages for ticket classification
   but does not attribute the commit to all co-authors in the identity or stats system.

---

## 1. PRs Merged

### Status: TRACKED (with caveats)

**Implementation:**
- File: `src/gitflow_analytics/integrations/github_integration.py`, lines 280–286
- The `_get_pull_requests()` method fetches with `state="all"` but immediately filters to
  `pr.merged and pr.merged_at >= since`. Only merged PRs are appended to the list.
- Cache key: `PullRequestCache` table (`src/gitflow_analytics/models/database.py`, lines 133–198),
  keyed on `(repo_path, pr_number)`, storing `merged_at`, `author`, `title`, `commit_hashes`.

**Who merged them:**
The `author` field stores `pr.user.login` (the PR author / person who opened the PR) —
**not** the person who clicked the Merge button. GitHub's merger identity is not captured.
`merged_by` is not fetched from the API at all.

**When merged:**
`merged_at` timestamp is stored and used for date-range filtering.

**Weaknesses:**
- "Merged by" (the approver who clicked merge) is not recorded.
- PR author stored as raw GitHub login string — not linked to canonical identity (see Section 5).

---

## 2. PRs Requested / Created

### Status: PARTIALLY TRACKED (aggregates only; no per-developer authored count)

**Implementation:**
- `_extract_pr_data()` (github_integration.py, lines 411–463) captures `created_at` and `author`
  (GitHub login) for every merged PR.
- `calculate_pr_metrics()` (lines 465–610) returns aggregate-only metrics:
  `total_prs`, `avg_pr_lifetime_hours`, etc. There is no per-developer authored count.

**"Own" vs "others'" distinction:** Not present. All metrics are team-level aggregates.

**Gaps:**
- No `prs_authored` counter per developer in `developer_stats` or any report.
- `created_at` is captured but no "PRs created in period" count is exposed in reports.
- PRs that were **opened but not yet merged** are not tracked (because of the
  `if pr.merged and pr.merged_at >= since` filter in `_get_pull_requests()`).

---

## 3. PRs Commented

### Status: PARTIALLY TRACKED (counts only, no per-developer attribution for comments on others' PRs)

**What IS tracked (when `fetch_pr_reviews: true`):**
- `review_comments` — inline code review comment count on the PR object (base PR data,
  always fetched). File: github_integration.py, line 452.
- `pr_comments_count` — general issue/PR timeline comments, fetched via
  `pr.get_issue_comments()`. File: github_integration.py, lines 366–373.
- `approvals_count`, `change_requests_count`, `reviewers`, `approved_by` — fetched via
  `_extract_review_data()` when `fetch_pr_reviews` is enabled (lines 301–382).

**What is NOT tracked:**
- **Which developer wrote which comment.** The comment counts are stored at the PR level
  (how many comments the PR received), not at the commenter level (how many comments
  developer X left on other people's PRs).
- `pr_comments_count` is gated behind `fetch_pr_reviews: true` config. By default it is
  zero for all PRs.
- The `reviewers` list (GitHub logins) is stored but never resolved through the
  `DeveloperIdentityResolver`, so it cannot be joined to canonical developer identities.
- There is no "comments left by this developer on others' PRs" metric anywhere.

**"Own" vs "others'" distinction:** Not present.

---

## 4. PRs Rejected / Closed Without Merge

### Status: NOT TRACKED (explicitly excluded)

**Implementation:**
File: `src/gitflow_analytics/integrations/github_integration.py`, lines 280–286

```python
# Only include PRs that were merged in our time period
if pr.merged and pr.merged_at >= since:
    prs.append(pr)
```

The API call uses `state="all"` (so closed PRs *are* fetched from GitHub), but the filter
`if pr.merged and pr.merged_at >= since` discards every PR where `pr.merged` is `False`.
Closed-without-merge PRs are fetched from the API and then thrown away.

**Schema:** The `PullRequestCache` table has no `state` or `closed_at` column
(database.py, lines 133–198). There is nowhere to persist closed-without-merge PRs even
if the filter were removed.

**`PullRequestData` data model** (`reports/data_models.py`, lines 121–197) does have:
- `state: str = "open"` (line 139)
- `is_merged: bool = False` (line 141)
- `closed_at: Optional[datetime] = None` (line 136)

But these fields are never populated from GitHub data because the integration drops closed
PRs before any extraction occurs.

**"Own" vs "others'" distinction:** Fully missing — there is no way to see closed/rejected
PRs at all, for either self or others.

---

## 5. Developer Alias / Identity Mapping

### Status: SOLID FOR GIT COMMITS — BROKEN FOR PR AUTHOR ATTRIBUTION

### 5a. Multiple Email Addresses (Git commits)

**Implementation:** `src/gitflow_analytics/core/identity.py`
- `DeveloperIdentityResolver.resolve_developer(name, email)` normalises email to lowercase
  (line 304), checks `DeveloperAlias` table by email (lines 316–329), then checks
  `DeveloperIdentity.primary_email` (lines 331–343), then tries fuzzy matching
  (`_find_best_match`, lines 360–414), then creates a new identity.
- Manual mappings in config: `analysis.identity.manual_mappings` list with
  `primary_email` and `aliases` (list of secondary emails). File: identity.py, lines 169–287.
- Multiple emails are merged into one `canonical_id`. The alias table is indexed on `email`.
- Case sensitivity: emails are `.lower().strip()` before all operations (line 304), so
  case-insensitive. Names are compared case-insensitively in fuzzy matching (line 366).

**Assessment: Good coverage for git commit authors.**

### 5b. GitHub Username vs. Git Author Name Mismatch

**Status: NOT HANDLED — major gap**

- `_extract_pr_data()` stores `pr.user.login` (GitHub username) in the `author` field
  (github_integration.py, line 443).
- `DeveloperIdentity` has a `github_username` column (database.py, line 98) but it is only
  populated when `_create_identity()` is called with an explicit `github_username` argument
  (identity.py, line 426). That argument is only passed from `resolve_developer()` when the
  caller supplies it — and **the GitHub integration never calls `resolve_developer()` for
  PR authors**. The PR author is stored as a raw login string and never joined to canonical IDs.
- `reviewers` and `approved_by` lists also store raw GitHub login strings.
- The `DeveloperAlias` table has no `github_username` column — there is no way to add a
  GitHub login as an alias.

**Consequence:** If a developer's GitHub username is `jdoe-gh` and their git author name is
`John Doe`, the system cannot link these. PRs authored by `jdoe-gh` float as unresolved strings
that never join to the commit-based developer identity.

**Config workaround:** None. The `manual_mappings` config supports email aliases only, not
GitHub usernames.

### 5c. Case Sensitivity

**Status: Handled for emails and names in git context**

- All email lookups are normalised to lowercase.
- Name comparisons use `.lower()` in cache key construction (identity.py, line 307) and
  fuzzy matching (line 365).
- However, GitHub logins are stored as-is (the `author` field in `PullRequestCache` is not
  lower-cased). GitHub logins are case-insensitive on GitHub's side, but the system stores
  them as returned by the API and never normalises them.

### 5d. Co-Author Trailers

**Status: STRIPPED for ticket classification; NOT resolved for identity attribution**

- File: `src/gitflow_analytics/extractors/tickets.py`, lines 35–54
- `filter_git_artifacts()` removes `Co-authored-by:` lines via regex before passing the
  message to ticket/classification systems.
- `intent_analyzer.py` lines 492–495 detect the presence of `Co-authored-by:` as a
  "collaboration signal" (`co_authored` type) but extracts no identity from it.
- **No code parses the co-author name/email and calls `resolve_developer()`**. Co-authors
  are not credited for commits, not counted in stats, not linked to canonical identities.

### 5e. Fuzzy Name Matching

**Status: Present but limited**

- `_find_best_match()` uses `difflib.SequenceMatcher` weighted by name similarity (40%),
  email domain match (30%), and alias name similarity (30%). Threshold default is 0.85.
- This works within the git commit author space but does not bridge to GitHub usernames.

---

## Gap Summary Table

| Capability | Implemented? | Own vs Others? | Key Gap |
|---|---|---|---|
| PRs merged — tracked | Yes (merged only) | No per-dev count | Merger identity (merged_by) missing |
| PRs merged — who merged | No | — | `merged_by` not fetched from API |
| PRs created / authored | Partial (aggregate) | No | No per-developer authored count |
| PR open (not yet merged) | No | — | Filtered out before storage |
| PR comments (as commenter) | No | No | Comments stored at PR level only |
| PR comments received | Partial (gated) | No | Requires `fetch_pr_reviews: true` |
| PRs reviewed (reviewer) | Partial (gated) | No | reviewer list stored but not resolved |
| PRs closed without merge | No | No | Explicitly filtered; no schema column |
| Multiple email aliases | Yes | — | Works well for git commits |
| GitHub username resolution | No | — | PR author never joins canonical identity |
| Co-author trailer attribution | No | — | Trailers stripped, not credited |
| Case sensitivity | Yes (emails/names) | — | GitHub logins not normalised |

---

## Actionable Recommendations

### Priority 1 — GitHub username to canonical identity bridge (Critical)

Add a `github_username` column to `DeveloperAlias` (or a separate lookup table) and resolve
PR authors against it.

**Recommended change in `github_integration.py` `_extract_pr_data()`:**
```python
# After extracting pr.user.login, look up or create canonical identity
github_login = pr.user.login
# Attempt to resolve via DeveloperIdentityResolver (needs to be injected)
canonical_id = identity_resolver.resolve_by_github_login(github_login)
pr_data["canonical_id"] = canonical_id
```

**Recommended change in `identity.py`:**
```python
def resolve_by_github_login(self, github_login: str) -> Optional[str]:
    """Resolve canonical ID from a GitHub username."""
    github_login_lower = github_login.lower()
    with self.get_session() as session:
        identity = session.query(DeveloperIdentity).filter(
            func.lower(DeveloperIdentity.github_username) == github_login_lower
        ).first()
        if identity:
            return identity.canonical_id
    return None
```

### Priority 2 — Track closed-without-merge PRs (High)

1. Add `state VARCHAR` and `closed_at DATETIME` to `PullRequestCache` schema.
2. In `_get_pull_requests()`, change the filter to capture closed PRs in addition to merged:
   ```python
   if pr.updated_at >= since and (pr.merged or (pr.state == "closed" and not pr.merged)):
       prs.append(pr)
   ```
3. In `_extract_pr_data()`, populate `state` field from `pr.state`.
4. Expose `prs_closed_not_merged` count in `calculate_pr_metrics()`.

### Priority 3 — Per-developer PR counts in reports (High)

Add per-developer PR attribution in the analytics pipeline:

```python
# In calculate_pr_metrics or a new function:
per_developer = defaultdict(lambda: {"prs_authored": 0, "prs_reviewed": 0})
for pr in prs:
    canonical_id = pr.get("canonical_id")  # after Priority 1 fix
    if canonical_id:
        per_developer[canonical_id]["prs_authored"] += 1
    for reviewer in pr.get("reviewers", []):
        reviewer_canonical = identity_resolver.resolve_by_github_login(reviewer)
        if reviewer_canonical:
            per_developer[reviewer_canonical]["prs_reviewed"] += 1
```

Surface these counts in `developer_stats` so they appear in CSV and narrative reports.

### Priority 4 — Per-developer PR comment attribution (Medium)

When `fetch_pr_reviews: true`, iterate `pr.get_issue_comments()` and extract `comment.user.login`
to credit individual commenters. Store a `comments_left_on_others_prs` counter per developer.

### Priority 5 — Co-author trailer attribution (Medium)

Parse `Co-authored-by: Name <email>` trailers in commit messages and call
`resolve_developer(name, email)` for each co-author, incrementing their commit stats.

```python
import re
CO_AUTHOR_PATTERN = re.compile(
    r"Co-authored-by:\s*(.+?)\s*<([^>]+)>", re.IGNORECASE
)
for match in CO_AUTHOR_PATTERN.finditer(commit_message):
    co_author_name, co_author_email = match.group(1), match.group(2)
    canonical_id = identity_resolver.resolve_developer(co_author_name, co_author_email)
    # credit commit to co-author
```

### Priority 6 — Normalise GitHub login casing (Low)

In `_extract_pr_data()` and `_extract_review_data()`, store `pr.user.login.lower()` consistently
to prevent case-mismatch issues when matching GitHub logins.

---

## Files Requiring Changes

| File | Change Required |
|---|---|
| `src/gitflow_analytics/integrations/github_integration.py` | Inject identity_resolver; resolve PR authors; track closed PRs; lower-case logins |
| `src/gitflow_analytics/core/identity.py` | Add `resolve_by_github_login()`; add `github_username` to alias lookup |
| `src/gitflow_analytics/models/database.py` | Add `state`, `closed_at`, `canonical_id` to `PullRequestCache`; add `github_username` to `DeveloperAlias` |
| `src/gitflow_analytics/core/cache.py` | Update `cache_pr()` to persist new fields |
| `src/gitflow_analytics/reports/csv_writer.py` | Add per-developer PR columns to developer summary |
| `src/gitflow_analytics/reports/narrative_writer.py` | Reference per-developer PR counts in narrative |
| `src/gitflow_analytics/core/data_fetcher.py` | Parse co-author trailers; attribute commits to co-authors |
