# QA Report: gitflow-analytics Data Collection & Classification
## Week of 2026-02-16 to 2026-02-22

**Date of QA**: 2026-02-26
**Config**: /Users/masa/Duetto/repos/gitflow-all-engineers.yaml
**Cache DB**: /Users/masa/Duetto/repos/.gitflow-cache/gitflow_cache.db
**Repos analyzed**: 146 repos in /Users/masa/Duetto/repos

---

## QA Check 1: Commit Count Verification — PASS

**Finding**: 918 commits confirmed in the SQLite cache for the week of 2026-02-16 to 2026-02-22.

```sql
SELECT COUNT(*) FROM cached_commits
WHERE timestamp >= '2026-02-16' AND timestamp < '2026-02-23';
-- Result: 918
```

- Total commits span: 2025-12-01 to 2026-02-22 (2,704 total in DB)
- Active repos with commits this week: 44 out of 146
- Daily batch records: 137 batch rows covering 918 commits
- Date range integrity: MIN = 2026-02-16 00:42, MAX = 2026-02-22 23:48 ✓

---

## QA Check 2: Classification Coverage — FAIL (Critical Bug Found)

**Finding**: 0% of commits are classified in the primary classification tables. The `gfa classify` command reported "2,488 commits classified" but this number is misleading.

### What the "2,488" number actually means

The classify command ran over 4 weeks (Jan 26 - Feb 22) and counted 2,513 commits in daily batch records across that range. The `total_commits` counter in `classify_date_range()` increments as it processes commits, but **classification results are never persisted to the database**.

### Root cause (code bug)

In `batch_classifier_impl.py:_store_commit_classification()`:

```python
def _store_commit_classification(self, session, classification_result):
    commit_hash = classification_result["commit_hash"]
    cached_commit = session.query(CachedCommit).filter(...).first()
    if cached_commit:
        # Builds a dict but NEVER assigns it to any field:
        {
            "category": classification_result["category"],
            "confidence": classification_result["confidence"],
            ...
        }
        # No: cached_commit.some_field = ...
        # No: session.add(new_row)
        # The dict is constructed and immediately garbage collected
```

Additionally, the `_classify_weekly_batches()` method opens its **own** session, but the `DailyCommitBatch` objects passed in were loaded and detached from a **different** session (in `_get_batches_to_process()`). So `batch.classification_status = "completed"` followed by `session.commit()` has no effect on the actual DB rows.

### Evidence

- `qualitative_commits` table: **0 rows**
- `classification_batches` table: **0 rows**
- `daily_commit_batches.classification_status` for all 365 rows: **"pending"** (none are "completed")
- `ml_predictions.db`: 681 rows created Feb 25 (separate cache used by LLM classifier lookup)

### Partial classification via ML predictions cache

The `ml_predictions.db` has 681 rule-based classifications created Feb 25. Of these, 153 can be matched to this week's 918 commits (16.7%) by message hash:

| Category       | Count | % of matched |
|---------------|-------|-------------|
| feature        | 38    | 24.8%       |
| other          | 32    | 20.9%       |
| bug_fix        | 27    | 17.6%       |
| infrastructure | 17    | 11.1%       |
| deployment     | 8     | 5.2%        |
| documentation  | 7     | 4.6%        |
| chore          | 4     | 2.6%        |
| configuration  | 3     | 2.0%        |
| style          | 3     | 2.0%        |
| ui             | 3     | 2.0%        |
| maintenance    | 2     | 1.3%        |
| refactor       | 2     | 1.3%        |
| other (minor)  | 13    | 8.5%        |

**Classification gap**: 765 commits (83.3%) have no classification stored.

---

## QA Check 3: Top Contributors Spot-Check — PASS (with alias caveat)

**Finding**: Top contributor numbers match previous run when aliases are merged. Raw DB shows splits.

### As stored in DB (raw, split identities):

| Rank | Name                   | Email                                    | Commits |
|------|------------------------|------------------------------------------|---------|
| 1    | Bob Matsuoka           | bob@matsuoka.com                         | 76      |
| 2    | Andrew Crane-Droesch   | andrew_crane.droesch@duettoresearch.com  | 58      |
| 3    | andreramosduetto       | andre.ramos@duettoresearch.com           | 29      |
| 4    | markbuchthal-duetto    | mark.buchthal@duettoresearch.com         | 29      |
| 5    | FluxCD Bot             | fluxcdbot@duetto.com                     | 25 (bot)|
| 6    | smc-cloud              | shane.catts@duettoresearch.com           | 25      |

### After manual alias consolidation:

| Rank | Canonical Name        | Commits | Note                          |
|------|-----------------------|---------|-------------------------------|
| 1    | Bob Matsuoka          | 82      | +6 from robert.matsuoka@...   |
| 2    | Andrew Crane-Droesch  | 72      | +14 from acd-duetto           |
| 3    | Mark Buchthal         | 38      | +9 from "Mark Buchthal" alias |
| 4    | Andre Ramos           | 29      | Single email                  |
| 5    | Jon Sikes Ham         | 26      | +5 from mac.local address     |

**Cross-reference vs previous run** (Bob: 76, Andrew: 58, Mark: 38):
- Mark Buchthal **38** matches exactly (29+9) ✓
- Bob Matsuoka now **82** (was 76) — the 6 "Bob" commits from robert.matsuoka@ are new
- Andrew Crane-Droesch now **72** (was 58) — 14 acd-duetto commits unaliased

---

## QA Check 4: Top Projects Spot-Check — PASS

**Finding**: Top projects match previous run exactly.

| Rank | Project                  | Commits | Prev Run | Match? |
|------|--------------------------|---------|----------|--------|
| 1    | DUETTO_FRONTEND          | 105     | 105      | ✓      |
| 2    | ML_ELASTICITY            | 97      | 97       | ✓      |
| 3    | NIFI_WORKFLOWS           | 68      | 68       | ✓      |
| 4    | OPS_TF                   | 60      | —        | New    |
| 5    | DUETTO_CODE_INTELLIGENCE | 54      | —        | New    |
| 6    | DUETTO_GITOPS            | 51      | —        | New    |
| 7    | INTEGRATION_ACCELERATION | 49      | —        | New    |
| 8    | DUETTO_INFRA             | 44      | —        | New    |
| 9    | DUETTO_SHARED_JAVASCRIPT | 41      | —        | New    |
| 10   | INTEGRATIONS             | 39      | —        | New    |

Top 3 match exactly. ✓

---

## QA Check 5: Developer Identity Resolution — FAIL (Aliases Not Configured)

**Finding**: 13 cases of identity fragmentation (same email, different display names). Plus 3 cases of same-person using different email addresses. The `developer_identities` and `developer_aliases` tables in the DB are both empty.

### Same-email name splits (same email, different git config names):

| Email                                       | Names (commits each)                          | Total |
|---------------------------------------------|-----------------------------------------------|-------|
| mark.buchthal@duettoresearch.com            | markbuchthal-duetto (29), Mark Buchthal (9)   | 38    |
| andrew_crane.droesch@duettoresearch.com     | Andrew Crane-Droesch (58), acd-duetto (14)*   | —     |
| fernando.sousa@duettoresearch.com           | Fernando Sousa (12), fernandosousa-duetto (10)| 22    |
| guilherme.nunes-c@duettoresearch.com        | guilhermeamaral (17), Guilherme Amaral (1)    | 18    |
| suraj.thapa@duettoresearch.com              | Suraj Thapa (19), sthapa-duetto (4)           | 23    |
| matthew.moulds@duettoresearch.com           | matthew-moulds (18), Matthew Moulds (1)       | 19    |
| mila.nambiar@duettoresearch.com             | milaremote (16), Mila (5)                     | 21    |
| elvin@duettoresearch.com                    | Elvin Ha (6), elvinduetto (1)                 | 7     |
| 26504836+cammyoung@users.noreply.github.com | Cam Young (12), Cameron Young (3)             | 15    |
| salva.vilaseca@duettoresearch.com           | Salva Vilaseca (2), salvavilaseca-duetto (2)  | 4     |
| sunny.kumar-c@duettoresearch.com            | sunny-duetto (2), encdxpahdlt0733 (1)         | 3     |
| jaya.vellayan@duettoresearch.com            | Jaya Vellayan (1), jvellayan-duetto (1)       | 2     |
| surya.subudhi-c@duettoresearch.com          | Surya Narayan Subudhi (1), suryasubudhi-c-debug (1) | 2 |

*Andrew uses two different email addresses (andrew. vs andrew_crane.) — different email, same person.

### Multi-email cases (different emails, same person):

| Person               | Email 1                                  | Email 2                                  | Combined |
|----------------------|------------------------------------------|------------------------------------------|----------|
| Bob Matsuoka         | bob@matsuoka.com (76)                    | robert.matsuoka@duettoresearch.com (6)   | 82       |
| Andrew Crane-Droesch | andrew_crane.droesch@duettoresearch.com  | andrew.crane-droesch@duettoresearch.com  | 72       |
| Jon Sikes Ham        | sikesham@mac.lan (21)                    | sikesham@Duetto-Jon-Hams-Mac.local (5)   | 26       |

**Current config**: Only Andre Ramos has an alias configured in gitflow-all-engineers.yaml. All other developers are fragmented.

---

## QA Check 6: Data Issues — MIXED

### Merge commits (14.3% of 918): WARNING

131 merge commits are included in the 918. These inflate commit counts without representing real work.

Repos with high merge commit ratios:
- **NIFI_WORKFLOWS**: 18/68 = 26.5% merge commits
- **ML_ELASTICITY**: 14/97 = 14.4% merge commits
- **OPS_TF**: 8/60 = 13.3% merge commits
- **DUETTO_FRONTEND**: 9/105 = 8.6% merge commits

The analyzer config does not specify `exclude_merge_commits: true`. These are included in developer commit counts.

### Bot commits (57 total, 6.2% of 918): WARNING

Bots are NOT filtered at collection time. The config has no `exclude_authors` or bot filter settings.

| Bot                      | Commits |
|--------------------------|---------|
| FluxCD Bot               | 25      |
| dependabot[bot]          | 20      |
| copilot-swe-agent[bot]   | 4       |
| snyk-bot                 | 4       |
| storybook-release[bot]   | 2       |
| Copilot                  | 1       |
| github-actions[bot]      | 1       |

**duetto-gitops** is particularly affected: 25 out of 51 commits (49%) are FluxCD Bot commits.

### Repos with suspiciously high counts (>50 commits/week): PASS

All high-count repos have plausible explanations:
- DUETTO_FRONTEND (105): Active frontend project with many contributors (9 merge, 5 bot)
- ML_ELASTICITY (97): Active ML repo, 14 merge commits explain some inflation
- NIFI_WORKFLOWS (68): Batch processing workflows, 18 merge commits
- OPS_TF (60): Infrastructure Terraform, expected high activity
- DUETTO_GITOPS (51): 25/51 are FluxCD bot commits — actual human commits = 26

---

## Summary Table

| QA Check                          | Status  | Notes                                                     |
|-----------------------------------|---------|-----------------------------------------------------------|
| 1. Commit count: 918              | PASS    | Exactly 918 in DB for Feb 16-22                           |
| 2. Classification coverage        | FAIL    | Bug: 0% persisted. _store_commit_classification() is a no-op |
| 3. Top contributors               | PASS*   | Numbers match after alias merging                         |
| 4. Top projects                   | PASS    | Top 3 exact match                                         |
| 5. Developer aliasing             | FAIL    | 13 same-email splits, 3 cross-email splits unresolved     |
| 6a. Merge commits                 | WARNING | 131 merges (14.3%) included — consider exclusion          |
| 6b. Bot commits                   | WARNING | 57 bot commits (6.2%) not filtered                        |
| 6c. High-count repos              | PASS    | All explainable                                           |

---

## Action Items

### Critical (classification is broken)

1. **Fix `_store_commit_classification()`** in `batch_classifier_impl.py`: The method builds a classification dict but never stores it. Either write to `CachedCommit.ticket_references` field as originally commented, or insert a row into `qualitative_commits`.

2. **Fix detached session bug** in `_classify_weekly_batches()`: Batch objects loaded via `_get_batches_to_process()` are from a closed session. The `classification_status = "completed"` updates are lost. Either pass/reuse the same session or reload objects within the new session.

### High priority (identity resolution)

3. **Add aliases to config** for at least: Mark Buchthal, Andrew Crane-Droesch, Fernando Sousa, Guilherme Amaral, Suraj Thapa, Matthew Moulds, Mila Nambiar. The same-email cases can be resolved purely by canonical name config.

4. **Add Bob Matsuoka cross-email alias**: `robert.matsuoka@duettoresearch.com` should alias to `bob@matsuoka.com`.

### Medium priority (data hygiene)

5. **Configure bot exclusions**: Add `FluxCD Bot`, `dependabot[bot]`, `snyk-bot`, `storybook-release[bot]`, `github-actions[bot]` to an exclude list. `duetto-gitops` is 49% bot commits.

6. **Consider merge commit exclusion**: 131 merges inflate counts. Add `exclude_merge_commits: true` to collection config or at minimum note that merge commits are included in report headers.
