# Developer Identity & Alias Mapping System Research

**Date:** 2026-02-26
**Scope:** Identity resolution pipeline, alias system, LLM integration, config state

---

## 1. Current Alias System — Data Model

### Database Layer (SQLite via SQLAlchemy)

The identity system persists to a SQLite database at `{cache.directory}/identities.db` with two tables:

- **`DeveloperIdentity`** — canonical records: `canonical_id` (UUID), `primary_name`, `primary_email`, `github_username`, `total_commits`, `total_story_points`, `first_seen`, `last_seen`
- **`DeveloperAlias`** — per-(email, name) alias rows: `canonical_id` (FK), `name`, `email`

### In-memory Cache

The `DeveloperIdentityResolver` loads all identities and aliases into `self._cache` as:
- `{email}:{name_lower}` → `canonical_id`  (alias lookup)
- `{canonical_id}` → `{primary_name, primary_email, github_username}` (identity data)

### Key Class: `DeveloperIdentityResolver`

**File:** `src/gitflow_analytics/core/identity.py`

Constructed with:
- `db_path`: path to SQLite DB
- `similarity_threshold`: default `0.85`
- `manual_mappings`: list of dicts from config

---

## 2. Identity Resolution Pipeline

### Resolution Sequence (per commit author)

```
resolve_developer(name, email, github_username)
    |
    ├─ 1. Normalize inputs (strip, lowercase email)
    |
    ├─ 2. Check in-memory cache: f"{email}:{name.lower()}" → canonical_id
    |         (cache hit → update stats, return)
    |
    ├─ 3. Query DB: DeveloperAlias WHERE email = email
    |         (found → cache it, add name variant as alias if different, return)
    |
    ├─ 4. Query DB: DeveloperIdentity WHERE primary_email = email
    |         (found → add name variant as alias if different, cache, return)
    |
    ├─ 5. _find_best_match(name, email):
    |         Score = name_similarity*0.4 + domain_match*0.3 + alias_match*0.3
    |         Threshold: similarity_threshold (default 0.85)
    |         (score >= threshold → _add_alias, cache, return)
    |
    └─ 6. _create_identity(name, email, github_username)
              (no match → new canonical identity, return new canonical_id)
```

### Manual Mappings Application

At startup, `_apply_manual_mappings(manual_mappings)` processes each mapping:
1. Find or create the canonical identity by `primary_email`
2. If the `name` field differs from `primary_name`, update it
3. For each alias email:
   - If alias email exists as a separate `DeveloperIdentity` → **merge** both into canonical
   - Otherwise → add a `DeveloperAlias` row

### GitHub Username Resolution

`resolve_by_github_username(github_username)` enables PR review attribution — looks up `DeveloperIdentity.github_username` (stored lowercase).

---

## 3. Config YAML Format for Manual Mappings

### Modern Format (recommended) — `analysis.identity.manual_mappings`

```yaml
analysis:
  identity:
    similarity_threshold: 0.85
    auto_analysis: true
    manual_mappings:
      - name: "Bob Matsuoka"           # Display name in reports
        primary_email: "robert.matsuoka@duettoresearch.com"  # Canonical email
        aliases:
          - "bob@matsuoka.com"         # All other emails this person uses

      - name: "Andrew Crane-Droesch"
        primary_email: "andrew.crane-droesch@duettoresearch.com"
        aliases:
          - "acd-duetto"               # git committer name variant (not email)

      # LLM-generated aliases also include confidence/reasoning:
      - name: "Jane Smith"
        primary_email: "jane.smith@company.com"
        aliases:
          - "jsmith@gmail.com"
        confidence: 0.92
        reasoning: "Same name, GitHub noreply pattern matches"
```

### Legacy Format — top-level `developer_aliases`

The config currently uses the legacy format. The loader **auto-converts** it at runtime:

```yaml
developer_aliases:
  "Andre Ramos":
    - "andre.ramos@duettoresearch.com"
    - "129991831+andreramosduetto@users.noreply.github.com"
```

The legacy format interprets the list items as both emails AND git name variants (it is not strictly typed). The conversion maps first list item as `primary_email`, all items as `aliases`.

**Important limitation:** The legacy format treats all entries as emails. Name variants like `"markbuchthal-duetto"` are passed as the `alias_email` — which works only if that exact string appears as the git committer email or name. For entries like `"Mark Buchthal"` → `["markbuchthal-duetto", "Mark Buchthal"]`, the system attempts to add these as alias emails, which may not match anything if those are git names rather than emails.

### External Aliases File — `analysis.identity.aliases_file`

```yaml
analysis:
  identity:
    aliases_file: aliases.yaml   # Relative to config dir
```

**aliases.yaml format:**
```yaml
# Developer Identity Aliases
# Generated by GitFlow Analytics
developer_aliases:
  - primary_email: "john@company.com"
    aliases:
      - "jdoe@gmail.com"
      - "12345+johndoe@users.noreply.github.com"
    name: "John Doe"
    confidence: 0.95              # Only for LLM-generated; omitted for manual (=1.0)
    reasoning: "GitHub noreply matches name"
```

Two formats supported in aliases.yaml:
1. `developer_aliases` list (recommended)
2. `teams` structure with nested `members` → `emails` lists

---

## 4. `gfa aliases` Command — LLM Integration

**File:** `src/gitflow_analytics/cli_identity_commands.py` → `aliases_command()`

### What It Does

1. Reads all commit history (last N weeks, default 12)
2. Extracts unique (email, name) pairs from commits
3. Runs `LLMIdentityAnalyzer.analyze_identities(commits)`
4. Saves result clusters to `aliases.yaml`

### LLM Provider Used

**CRITICAL FINDING: `gfa aliases` uses OPENROUTER only, NOT Bedrock.**

```python
# cli_identity_commands.py lines 393-403
api_key = None
if cfg.chatgpt and cfg.chatgpt.api_key:
    api_key_value = cfg.chatgpt.api_key
    ...
    api_key = api_key_value

if not api_key:
    click.echo("Warning: No OpenRouter API key configured - using heuristic analysis only")

llm_analyzer = LLMIdentityAnalyzer(api_key=api_key, ...)
```

The `LLMIdentityAnalyzer` hardcodes OpenRouter:
```python
# identity_llm/analyzer.py line 270
client = openai.OpenAI(base_url="https://openrouter.ai/api/v1", api_key=self.api_key)
```

It looks for `cfg.chatgpt.api_key` (the legacy `chatgpt:` config section), not `cfg.analysis.llm_classification`. **The Bedrock provider configured for commit classification is NOT used for alias generation.**

### `gfa identities` Command

Uses `IdentityAnalysisPass` which looks for the OpenRouter key in `config["qualitative"]["openrouter_api_key"]` — also NOT Bedrock.

### Heuristic Fallback (no API key)

When no OpenRouter key exists, `LLMIdentityAnalyzer` uses pure heuristics:

```python
_are_likely_same_developer(id1, id2):
    - GitHub noreply pattern matching (extracts username, compares to name/local)
    - Email local part vs name substring matching
    - Company suffix stripping: "-ewtn", "-zaelot", "dev", "developer", "zaelot"
    - Name similarity > 0.9 + same domain
    - Name similarity > 0.8 + GitHub domain
    - Email local part in name parts
    - Initials match + name similarity > 0.6
    - Overlapping repositories + name similarity > 0.7
```

The hardcoded suffix stripping (`"-zaelot"`, `"-ewtn"`) is company-specific and won't help with Duetto-specific suffixes like `-duetto`.

---

## 5. Current State of Aliases in Config

**File:** `~/Duetto/repos/gitflow-all-engineers.yaml`

Uses the **legacy `developer_aliases` format** with 12 entries:

| Canonical Name | Type | Notes |
|---|---|---|
| Andre Ramos | cross-email | real email + GitHub noreply |
| Mark Buchthal | name variant | "markbuchthal-duetto" as name |
| Fernando Sousa | name variant | "fernandosousa-duetto" as name |
| Suraj Thapa | name variant | "sthapa-duetto" as name |
| Guilherme Amaral | name variant | "guilhermeamaral" as name |
| Matthew Moulds | name variant | "matthew-moulds" as name |
| Mila Nambiar | name variant | "milaremote", "Mila" |
| Cam Young | name variant | "Cameron Young" |
| Elvin Ha | name variant | "elvinduetto" as name |
| Bob Matsuoka | cross-email | personal + company email |
| Andrew Crane-Droesch | name variant | "acd-duetto" as name |
| Jon Sikes Ham | cross-email | two local machine emails |

**No `chatgpt:` section exists in the config** → `gfa aliases` will run in heuristic-only mode.

**The `-duetto` suffix pattern is NOT in the hardcoded strip list** in `_are_likely_same_developer`. This is a key gap.

---

## 6. Where Alias Gaps Occur

### Gap Categories

**A. Different emails, same person**
- Personal email vs company email (e.g., `bob@matsuoka.com` vs `robert.matsuoka@duettoresearch.com`)
- GitHub noreply (`12345+username@users.noreply.github.com`) vs company email
- Old domain vs new domain after company rename

**B. Git username vs email mismatch**
- Developer sets `git config user.name "fernandosousa-duetto"` with company email → name variant
- Developer uses different machines with different git configs
- The `-duetto` suffix pattern affects many entries but is not stripped in heuristics

**C. Name formatting variations**
- "J. Smith" vs "John Smith" vs "johnsmith" (similarity < 0.85 threshold)
- Abbreviated names: "Mila" vs "Mila Nambiar"
- First-name-only commits

**D. GitHub Actions attribution**
- PR reviews/approvals by GitHub username won't resolve unless `github_username` is set on the identity

**E. Legacy format limitations**
- The legacy `developer_aliases` format treats all list entries as `aliases` (used as emails)
- Name-only variants like `"markbuchthal-duetto"` won't match if it appears as git `user.name`, not `user.email`
- The modern format with `primary_email` + `aliases` (emails only) is more precise

---

## 7. Recommendations

### Immediate: Fix the Config Format

Migrate from legacy `developer_aliases` to `analysis.identity.manual_mappings`. The legacy format has an ambiguity problem — it treats name variants and emails identically, but the resolver only does exact email matching, not name-based alias lookup in `_apply_manual_mappings`.

For name variants (not emails), the match happens only via the fuzzy `_find_best_match` which requires similarity >= 0.85. A developer with git name `"sthapa-duetto"` vs canonical name `"Suraj Thapa"` scores approximately 0.35 name similarity — far below threshold.

**Correct format for name variants:**
```yaml
analysis:
  identity:
    manual_mappings:
      # For cross-email merges:
      - name: "Bob Matsuoka"
        primary_email: "robert.matsuoka@duettoresearch.com"
        aliases:
          - "bob@matsuoka.com"

      # For name variants (different git user.name, same email):
      # These won't work as "aliases" — aliases are email-based.
      # The system needs to see them as commit authors with the same email.
      # Name variants on the same email are auto-detected during resolve_developer().
```

**Key insight:** The `aliases` list in `manual_mappings` expects EMAIL addresses, not git names. Name variants (same email, different `git config user.name`) are handled automatically by `resolve_developer` — when the same email is seen with a different name, it's automatically added as a `DeveloperAlias` row (line 383 in identity.py).

### Fix: Add `-duetto` to Heuristic Suffix Stripping

In `identity_llm/analyzer.py` line 183, add `"-duetto"` to the suffix list:
```python
for suffix in ["-ewtn", "-zaelot", "dev", "developer", "zaelot", "-duetto"]:
```

### Fix: Enable LLM for `gfa aliases`

Option 1 — Add OpenRouter key to config:
```yaml
chatgpt:
  api_key: "${OPENROUTER_API_KEY}"
  model: "openai/gpt-4o-mini"
```

Option 2 (recommended) — Fix `gfa aliases` to use the Bedrock provider that's already configured. The code gap is in `cli_identity_commands.py` lines 391-409: it checks `cfg.chatgpt` but should also check `cfg.analysis.llm_classification` and delegate to the existing `LLMCommitClassifier` Bedrock infrastructure.

### Run `gfa aliases` to Discover Missing Mappings

Even in heuristic-only mode:
```bash
gfa aliases -c ~/Duetto/repos/gitflow-all-engineers.yaml \
  --weeks 52 \
  --confidence-threshold 0.8 \
  -o ~/Duetto/repos/aliases.yaml
```

Then reference the file:
```yaml
analysis:
  identity:
    aliases_file: aliases.yaml
```

### Use `gfa create-alias-interactive` for Manual Gaps

For developers the heuristics miss, the interactive tool shows all discovered identities with commit counts and lets you manually merge them:
```bash
gfa create-alias-interactive -c ~/Duetto/repos/gitflow-all-engineers.yaml
```

---

## Summary Table

| Question | Answer |
|---|---|
| Data model | SQLite: `DeveloperIdentity` + `DeveloperAlias` tables |
| Resolution order | Cache → alias email match → primary email match → fuzzy score ≥ 0.85 → new identity |
| Manual mapping format | `analysis.identity.manual_mappings` (modern) or legacy `developer_aliases` |
| `aliases` field meaning | EMAIL addresses only (not git names) |
| `gfa aliases` LLM provider | OpenRouter via `chatgpt.api_key` — NOT Bedrock |
| Bedrock used for aliases? | NO — Bedrock only used for commit classification |
| Can Bedrock be used? | Yes, but requires code change in `cli_identity_commands.py` |
| Heuristic `-duetto` suffix | NOT handled — needs addition to strip list |
| Current alias count | 12 entries in legacy format |
| Missing alias root cause | Legacy format limitations + `-duetto` suffix not stripped + no LLM key |
