# Identity Resolution Architecture Research
**Date**: 2026-02-27
**Question**: Does changing aliases currently force a re-collect? What needs to change so raw data collection is decoupled from identity resolution?

---

## Executive Summary

The current architecture is **already well-separated**: raw commit data (author names/emails) is stored verbatim in the DB during collection, and identity resolution only runs at report time. However, **the config hash does NOT include alias/identity settings**, so changing aliases does NOT force a re-collect. The pipeline is architecturally sound but has a subtle correctness issue in `pipeline_report.py` that may cause stale identity data to appear in reports without a clear signal to the user.

---

## 1. What is Stored in the DB (CachedCommit)

**File**: `src/gitflow_analytics/models/database_commit_models.py`, class `CachedCommit`

The `cached_commits` table stores **raw, unresolved git author data**:

```
author_name   = Column(String)   # raw git commit author name
author_email  = Column(String)   # raw git commit author email
```

There is **no `canonical_id`, `canonical_name`, or resolved identity** in this table. The DB is a faithful mirror of what `git log` returns. Identity resolution is deliberately absent from the storage model.

---

## 2. Where Identity Resolution Happens — Per Stage

### Stage 1: `pipeline_collect.py` (gfa collect)
- Zero identity resolution. No import or call to `DeveloperIdentityResolver`.
- Grep confirms: no matches for `identity`, `resolve_developer`, or `canonical` in this file.
- Stores raw `author_name`, `author_email` verbatim.

### Stage 2: `pipeline_classify.py` (gfa classify)
- Zero identity resolution. No import or call to `DeveloperIdentityResolver`.
- Grep confirms: no matches for `identity`, `resolve_developer`, or `canonical` in this file.
- Operates only on `CachedCommit` rows and `DailyCommitBatch` rows.

### Stage 3: `pipeline_report.py` (gfa report) — WHERE RESOLUTION HAPPENS
```python
# pipeline_report.py lines 107-113
identity_db_path = cfg.cache.directory / "identities.db"
identity_resolver = DeveloperIdentityResolver(
    identity_db_path,
    similarity_threshold=cfg.analysis.similarity_threshold,
    manual_mappings=cfg.analysis.manual_identity_mappings,  # <-- aliases from config
)
identity_resolver.update_commit_stats(all_commits)
```
- The resolver is instantiated with the **current** `manual_identity_mappings` from config.
- `update_commit_stats(all_commits)` walks all loaded commit dicts (which have raw `author_name`/`author_email`) and runs fuzzy matching + alias application.
- `developer_stats` produced here uses resolved canonical names for reporting.
- The commit dict gets `"canonical_id": cached_commit.author_email or "unknown"` as a trivial pre-resolution fallback (line 101), but this is overridden by the resolver.

### `core/analyze_pipeline.py` (gfa analyze — unified path)
Same pattern. Identity resolver is initialised in Stage 6 of the orchestrator (`cli_analysis_orchestrator.py` line 363), **after** the fetch stage, with `manual_mappings=cfg.analysis.manual_identity_mappings`. Resolution runs in `resolve_developer_identities()` (Stage 9).

---

## 3. The Config Hash — What It Covers

**File**: `src/gitflow_analytics/core/cache_weekly.py`, `generate_config_hash()` (line 563)

```python
config_data = {
    "branch_mapping_rules": branch_mapping_rules or {},
    "ticket_platforms": sorted(ticket_platforms or []),
    "exclude_paths": sorted(exclude_paths or []),
    "ml_categorization_enabled": ml_categorization_enabled,
    "additional_config": additional_config or {},
}
```

**Critical finding: `manual_identity_mappings` (aliases) are NOT in the config hash.**

The hash is called in:
- `pipeline_collect.py` (line 74) — determines whether to skip cached repos
- `cli_analysis_orchestrator.py` (line 526) — same purpose in the analyze path

Additional config included in the hash in the orchestrator path:
```python
additional_config={
    "weeks": weeks,
    "enable_qualitative": enable_qualitative,
    "enable_pm": enable_pm,
    "pm_platforms": ...,
    "exclude_merge_commits": cfg.analysis.exclude_merge_commits,
}
```

**Aliases/manual_mappings are absent from all config hash computations.**

---

## 4. Is Re-Collection Required When Aliases Change?

**No — and this is correct behaviour.** Because:

1. `CachedCommit` stores raw `author_name` + `author_email` from git.
2. The config hash does not include alias settings, so changing aliases does not invalidate the `RepositoryAnalysisStatus` cache.
3. `gfa collect` will skip already-cached repos regardless of alias changes.
4. `gfa report` re-applies the current aliases at report time from the current config.

**The user's concern that "we shouldn't have to recollect data when aliases are updated" is already satisfied at the architecture level.** Changing aliases does NOT trigger re-collection.

---

## 5. The Identity DB — Potential Stale-Data Issue

There IS a subtle correctness issue: `identities.db` is a **persistent SQLite database** (`DeveloperIdentityResolver` in `identity.py`). When `_apply_manual_mappings()` is called:

1. It checks if a canonical identity already exists in `identities.db` by email.
2. If the identity exists with an old `primary_name`, it updates it.
3. Old alias entries remain in `developer_aliases` table unless explicitly merged or deleted.

This means: if you rename a developer in `manual_mappings` (change their canonical display name), the old name may still exist as a `DeveloperAlias` row in `identities.db` until `merge_identities()` cleans it up. The next `gfa report` run re-applies mappings and should update `primary_name`, but stale alias rows can cause unexpected fuzzy-match behaviour.

This is **not a re-collection problem** — it's an identity-DB hygiene concern.

---

## 6. Summary of the Data Flow

```
git log (raw)
    |
    v
CachedCommit
  author_name = "john.smith"      <-- raw git name, never changed
  author_email = "j@old.co"      <-- raw git email, never changed
    |
    |  (collect stage ends here — no identity work)
    |
    v
pipeline_report.py
  DeveloperIdentityResolver(manual_mappings=cfg.analysis.manual_identity_mappings)
    |
    +-- resolves by email -> canonical_id
    +-- applies display name from manual_mappings["name"]
    +-- falls back to fuzzy matching
    |
    v
developer_stats (resolved, canonical names used in CSV/Markdown reports)
```

---

## 7. What Needs to Change

Given the user's concern, the architecture is already correct on the primary question. However, there are two improvements worth making:

### Issue A: `canonical_id` pre-assignment is email-only (not truly resolved)

In both `pipeline_report.py` (line 101) and `analyze_pipeline.py` (line 603):
```python
commit_dict["canonical_id"] = cc.author_email or "unknown"
```
This is a placeholder that equates email to canonical ID. The real resolution happens inside the `DeveloperIdentityResolver` when `update_commit_stats()` runs. This works correctly but is misleading — the field name suggests it's already resolved.

### Issue B: `identities.db` is not rebuilt when aliases change

The `DeveloperIdentity` and `DeveloperAlias` tables persist across runs. When aliases are updated in config, `_apply_manual_mappings()` upserts new mappings but does NOT purge stale/conflicting alias rows. A `--rebuild-identities` flag or a clear-identities-on-alias-change mechanism would give users confidence that the identity DB reflects the current config exactly.

### Issue C (non-issue, confirmed): Config hash correctly excludes aliases

Since aliases are applied at report time (not at collect time), it is correct that the config hash does not include alias settings. No change needed here.

---

## 8. Files Investigated

| File | Role |
|------|------|
| `src/gitflow_analytics/pipeline_collect.py` | Stage 1 — raw git fetch, no identity resolution |
| `src/gitflow_analytics/pipeline_classify.py` | Stage 2 — LLM classification, no identity resolution |
| `src/gitflow_analytics/pipeline_report.py` | Stage 3 — identity resolution runs here |
| `src/gitflow_analytics/core/analyze_pipeline.py` | Unified analyze path — identity resolution in Stage 9 |
| `src/gitflow_analytics/core/cache_weekly.py` | `generate_config_hash()` — aliases absent |
| `src/gitflow_analytics/core/identity.py` | `DeveloperIdentityResolver` — core resolution logic |
| `src/gitflow_analytics/models/database_commit_models.py` | `CachedCommit` — raw author_name/email only |
| `src/gitflow_analytics/models/database_identity_models.py` | `DeveloperIdentity`, `DeveloperAlias` — persistent identity store |
| `src/gitflow_analytics/config/loader.py` | Alias/mapping loading from config |
