# Research: Alias/Identity Infrastructure for `add-alias` CLI Command

**Date:** 2026-04-08  
**Purpose:** Understand existing infrastructure before implementing non-interactive `add-alias` command

---

## 1. Existing CLI Commands

### Registration entry point
- **File:** `src/gitflow_analytics/cli.py` (lines 112-119)
- **Pattern:** `from .cli_identity import register_identity_commands; register_identity_commands(cli)`

### Registration function
- **File:** `src/gitflow_analytics/cli_identity.py`, function `register_identity_commands(cli_group)`
- Registers: `merge_identity`, `identities`, `aliases_command`, `list_developers`, `create_alias_interactive`, `alias_rename`

### Command inventory

| Command | Decorator | File | Signature |
|---|---|---|---|
| `merge-identity` | `@click.command()` | `cli_identity.py:39` | `(config: Path, dev1: str, dev2: str)` — dev1=PRIMARY_EMAIL, dev2=ALIAS_EMAIL |
| `identities` | `@click.command()` | `cli_identity_commands.py:29` | `(config, weeks, apply)` |
| `aliases` | `@click.command(name="aliases")` | `cli_identity_commands.py:227` | `(config, output, confidence_threshold, apply, weeks)` |
| `list-developers` | `@click.command()` | `cli_identity.py:83` | `(config)` |
| `create-alias-interactive` | `@click.command(name="create-alias-interactive")` | `cli_identity_alias_ops.py:25` | `(config, output)` |
| `alias-rename` | `@click.command(name="alias-rename")` | `cli_identity_alias_ops.py:276` | `(config, old_name, new_name, update_cache, dry_run, interactive)` |

**New command should go in:** `cli_identity_alias_ops.py` (or `cli_identity_commands.py`), then registered in `register_identity_commands()` in `cli_identity.py`.

---

## 2. `developer_aliases` Config Block

### Schema
- **File:** `src/gitflow_analytics/config/schema.py`

`IdentityConfig` dataclass (line 316):
```python
@dataclass
class IdentityConfig:
    similarity_threshold: float = 0.85
    manual_mappings: list[dict[str, Any]] = field(default_factory=list)
    aliases_file: Optional[Path] = None
    auto_analysis: bool = True
    strip_suffixes: list[str] = field(default_factory=list)
```

Lives at `AnalysisConfig.identity` (type `IdentityConfig`), accessed as `cfg.analysis.identity`.

### YAML structure (modern format — `analysis.identity.manual_mappings`)
```yaml
analysis:
  identity:
    manual_mappings:
      - name: "John Doe"
        primary_email: "john@company.com"
        aliases:
          - john@gmail.com
          - 12345+johndoe@users.noreply.github.com
    aliases_file: aliases.yaml   # optional external file reference
```

### Legacy format (deprecated, auto-converted by loader)
```yaml
developer_aliases:
  "Chung Kim":
    - chungk-duetto
    - chung.kim@company.com
    - 61434073+chungk-duetto@users.noreply.github.com
```
Loader converts this → `analysis.identity.manual_mappings` automatically and deletes the old key.

---

## 3. How `developer_aliases` is Parsed (loader.py)

**File:** `src/gitflow_analytics/config/loader.py`

Key parsing flow (lines ~253-784):
1. Legacy `developer_aliases` top-level key detected → converted to `analysis.identity.manual_mappings`
2. `identity_raw = analysis_data.get("identity", {})` 
3. `manual_mappings = list(identity_raw.get("manual_mappings", []))`
4. If `aliases_file` present → load with `AliasesManager(aliases_path)`, call `.to_manual_mappings()`, extend `manual_mappings`
5. Build `IdentityConfig(manual_mappings=manual_mappings, aliases_file=aliases_file_path, ...)`
6. Also set `analysis.manual_identity_mappings = manual_mappings` (legacy field on AnalysisConfig)

---

## 4. `developer_aliases` DB Table

**File:** `src/gitflow_analytics/models/database_identity_models.py`

```python
class DeveloperAlias(Base):
    __tablename__ = "developer_aliases"

    id           = Column(Integer, primary_key=True)
    canonical_id = Column(String, nullable=False)   # FK to DeveloperIdentity.canonical_id
    name         = Column(String, nullable=False)   # Display name at time of alias creation
    email        = Column(String, nullable=False)   # The alias email address (stored lowercased)

    # Unique index on (name, email)
    __table_args__ = (
        Index("idx_alias_email", "email"),
        Index("idx_alias_canonical_id", "canonical_id"),
        Index("idx_name_email", "name", "email", unique=True),
    )
```

Note: There is NO `source` column, NO `alias_name` column, NO `canonical_email` column in the DB model. The columns are only: `id`, `canonical_id`, `name`, `email`.

`DeveloperIdentity` table (`developer_identities`):
- `id`, `canonical_id` (UUID string, unique), `primary_name`, `primary_email`, `github_username`
- `total_commits`, `total_story_points`, `first_seen`, `last_seen`, `created_at`, `updated_at`

---

## 5. How Aliases Are Written to DB

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

### Direct alias insert (`_add_alias` method, line ~580):
```python
alias = DeveloperAlias(canonical_id=canonical_id, name=name, email=email.lower())
session.add(alias)
```
Checks for existing before inserting (deduplication on `canonical_id + email`).

### Via `_apply_manual_mappings` (line 175):
For each mapping in `manual_mappings`:
1. Find or create `DeveloperIdentity` by `primary_email`
2. For each alias email:
   - If alias email is already a primary identity → call `merge_identities()`
   - Otherwise → `session.add(DeveloperAlias(canonical_id=..., name=..., email=alias_email))`

### Key method: `merge_identities(canonical_id1, canonical_id2)` (line 615)
- Accepts two `canonical_id` strings (UUIDs), not emails
- Adds identity2's primary as alias under identity1
- Reassigns all identity2's aliases to identity1
- Deletes identity2

### How `create_alias_interactive` writes to DB (cli_identity_alias_ops.py:237):
```python
identity_resolver.merge_identities(
    primary_dev["canonical_id"],
    alias_dev["canonical_id"],
)
```
Uses `canonical_id` from `get_developer_stats()` result dict.

---

## 6. How Aliases Are Written to Config YAML

**File:** `src/gitflow_analytics/config/aliases.py` — `AliasesManager.save()`

Pattern used by `aliases_command` (cli_identity_commands.py:520-541):
```python
with open(config) as f:
    config_data = yaml.safe_load(f)

config_data["analysis"]["identity"]["aliases_file"] = str(rel_path)

with open(config, "w") as f:
    yaml.dump(config_data, f, default_flow_style=False, sort_keys=False)
```

Pattern used by `alias_rename` (cli_identity_alias_ops.py:470-484):
```python
with open(config, "w", encoding="utf-8") as f:
    yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
```

### `AliasesManager.save()` — writes `aliases.yaml` external file:
```python
yaml.dump(
    {"developer_aliases": [alias.to_dict() for alias in self.aliases]},
    f,
    default_flow_style=False,
    sort_keys=False,
    allow_unicode=True,
)
```

### `DeveloperAlias.to_dict()` output format:
```python
{
    "primary_email": "john@company.com",
    "aliases": ["john@gmail.com"],
    # optional:
    "name": "John Doe",
    "confidence": 0.95,   # only if < 1.0
    "reasoning": "...",   # only if confidence < 1.0
}
```

---

## 7. `merge-identity` Command

**File:** `src/gitflow_analytics/cli_identity.py:39`

```python
@click.command()
@click.option("--config", "-c", type=click.Path(exists=True, path_type=Path), required=True)
@click.argument("dev1", metavar="PRIMARY_EMAIL")
@click.argument("dev2", metavar="ALIAS_EMAIL")
def merge_identity(config: Path, dev1: str, dev2: str) -> None:
```

Implementation:
```python
cfg = ConfigLoader.load(config)
identity_resolver = DeveloperIdentityResolver(cfg.cache.directory / "identities.db")
identity_resolver.merge_identities(dev1, dev2)
```

**PROBLEM:** `merge_identities()` takes `canonical_id` (UUID) as arguments, but `merge_identity` CLI passes email strings (`dev1`, `dev2`) directly. This works only if the email strings happen to match canonical_ids — this appears to be a bug or the method accepts both. (The `create_alias_interactive` command correctly uses `dev["canonical_id"]` from stats.)

---

## 8. `cli_identity_commands.py` — Full Command List

Commands defined in this file:
- `identities` (line 29) — LLM analysis, generates suggestions, optional `--apply`
- `aliases_command` (name=`aliases`, line 227) — generates `aliases.yaml` via LLM, optional `--apply` to update config

Both are imported and re-exported by `cli_identity.py`.

**This is where the new `add-alias` command should be added** (alongside `alias_rename` and `create_alias_interactive` in `cli_identity_alias_ops.py`).

---

## 9. Aliases YAML Format

### External `aliases.yaml` file format:
```yaml
# Developer Identity Aliases
# Generated by GitFlow Analytics
# Share this file across multiple config files
# Each alias maps multiple email addresses to a single developer

developer_aliases:
  - primary_email: john@company.com
    aliases:
      - john@gmail.com
      - 12345+johndoe@users.noreply.github.com
    name: John Doe

  - primary_email: jane@company.com
    aliases:
      - jane.smith@personal.io
    name: Jane Smith
    confidence: 0.92
    reasoning: Same name pattern across organizations
```

### Inline config format (`analysis.identity.manual_mappings`):
```yaml
analysis:
  identity:
    manual_mappings:
      - name: "John Doe"
        primary_email: "john@company.com"
        aliases:
          - john@gmail.com
```

Both `primary_email` and legacy `canonical_email` field names are accepted by parser.

---

## Key Classes / Functions Summary

| Symbol | File | Purpose |
|---|---|---|
| `DeveloperAlias` (model) | `models/database_identity_models.py` | DB table: `developer_aliases` |
| `DeveloperIdentity` (model) | `models/database_identity_models.py` | DB table: `developer_identities` |
| `DeveloperAlias` (dataclass) | `config/aliases.py` | In-memory alias representation |
| `AliasesManager` | `config/aliases.py` | Load/save `aliases.yaml` |
| `DeveloperIdentityResolver` | `core/identity.py` | DB operations, identity resolution |
| `_apply_manual_mappings()` | `core/identity.py:175` | Writes manual_mappings to DB |
| `_add_alias()` | `core/identity.py:~580` | Low-level alias insert to DB |
| `merge_identities()` | `core/identity.py:615` | Merges two identities by canonical_id |
| `register_identity_commands()` | `cli_identity.py:25` | Registers all commands onto CLI group |
| `IdentityConfig` | `config/schema.py:316` | Dataclass for `analysis.identity.*` |
