diff --git a/roadmap-planner/backend/internal/contributions/service_extras.go b/roadmap-planner/backend/internal/contributions/service_extras.go index f4bc1486..4d25de9c 100644 --- a/roadmap-planner/backend/internal/contributions/service_extras.go +++ b/roadmap-planner/backend/internal/contributions/service_extras.go @@ -493,7 +493,7 @@ func (s *Service) MemberExtras(ctx context.Context, memberID string, q storage.M // sprintCounts gathers (wip, done, prs_open, prs_merged) for a member // scoped to one sprint. WIP / Done come from the *latest* snapshot // per issue under that sprint id; PR counts come from pull_requests -// linked via epic_key (best-effort). +// linked via jira_key (best-effort). func (s *Service) sprintCounts(ctx context.Context, db *sql.DB, dialect storage.Dialect, memberID, sprintID string) (*SprintStats, error) { out := &SprintStats{Name: sprintID} // default name = id; we overwrite below if we see something better @@ -529,8 +529,8 @@ func (s *Service) sprintCounts(ctx context.Context, db *sql.DB, dialect storage. } // PR counts: look at pull_requests opened by this member that - // reference any issue assigned to the sprint via epic_key. This - // only works for projects where the linker resolves an epic key + // reference any issue assigned to the sprint via jira_key. This + // only works for projects where the linker resolves a jira key // onto every PR — when it doesn't, the counts stay at zero. prQ := rebindSimple(dialect, ` SELECT @@ -538,7 +538,7 @@ func (s *Service) sprintCounts(ctx context.Context, db *sql.DB, dialect storage. SUM(CASE WHEN merged_at IS NULL AND closed_at IS NULL THEN 1 ELSE 0 END) FROM pull_requests WHERE author_id = ? - AND epic_key IN ( + AND jira_key IN ( SELECT DISTINCT issue_key FROM issue_snapshots WHERE sprint_id = ? AND assignee_id = ? )`) diff --git a/roadmap-planner/backend/internal/github/sync.go b/roadmap-planner/backend/internal/github/sync.go index 8ed5eb34..53fb3e48 100644 --- a/roadmap-planner/backend/internal/github/sync.go +++ b/roadmap-planner/backend/internal/github/sync.go @@ -221,7 +221,7 @@ func (s *Syncer) Sync(ctx context.Context) error { Additions: pr.Additions, Deletions: pr.Deletions, ChangedFiles: pr.ChangedFiles, - EpicKey: epicKey, + JiraKey: epicKey, CreatedAt: pr.CreatedAt, MergedAt: pr.MergedAt, ClosedAt: pr.ClosedAt, diff --git a/roadmap-planner/backend/internal/gitlab/sync.go b/roadmap-planner/backend/internal/gitlab/sync.go index dfaaf25b..6c41e89c 100644 --- a/roadmap-planner/backend/internal/gitlab/sync.go +++ b/roadmap-planner/backend/internal/gitlab/sync.go @@ -248,7 +248,7 @@ func (s *Syncer) Sync(ctx context.Context) error { AuthorLogin: authorLogin, HeadBranch: mr.SourceBranch, BaseBranch: mr.TargetBranch, - EpicKey: epicKey, + JiraKey: epicKey, CreatedAt: mr.CreatedAt, MergedAt: mr.MergedAt, ClosedAt: mr.ClosedAt, diff --git a/roadmap-planner/backend/internal/storage/generic.go b/roadmap-planner/backend/internal/storage/generic.go index ccbf88a6..8fe6a7e5 100644 --- a/roadmap-planner/backend/internal/storage/generic.go +++ b/roadmap-planner/backend/internal/storage/generic.go @@ -124,7 +124,7 @@ func (s *genericStore) UpsertPullRequests(ctx context.Context, prs []PullRequest INSERT INTO pull_requests ( id, source, repo_id, number, title, state, author_id, author_login, head_branch, base_branch, additions, deletions, changed_files, - epic_key, created_at, first_review_at, merged_at, closed_at, fetched_at + jira_key, created_at, first_review_at, merged_at, closed_at, fetched_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET source = excluded.source, @@ -135,7 +135,7 @@ func (s *genericStore) UpsertPullRequests(ctx context.Context, prs []PullRequest additions = excluded.additions, deletions = excluded.deletions, changed_files = excluded.changed_files, - epic_key = excluded.epic_key, + jira_key = excluded.jira_key, first_review_at = excluded.first_review_at, merged_at = excluded.merged_at, closed_at = excluded.closed_at, @@ -153,7 +153,7 @@ func (s *genericStore) UpsertPullRequests(ctx context.Context, prs []PullRequest _, err := stmt.ExecContext(ctx, p.ID, source, p.RepoID, p.Number, p.Title, p.State, nullable(p.AuthorID), nullable(p.AuthorLogin), nullable(p.HeadBranch), nullable(p.BaseBranch), p.Additions, p.Deletions, p.ChangedFiles, - nullable(p.EpicKey), p.CreatedAt, p.FirstReviewAt, p.MergedAt, p.ClosedAt, p.FetchedAt, + nullable(p.JiraKey), p.CreatedAt, p.FirstReviewAt, p.MergedAt, p.ClosedAt, p.FetchedAt, ) if err != nil { return fmt.Errorf("upsert pr %s: %w", p.ID, err) diff --git a/roadmap-planner/backend/internal/storage/migrations/0006_rename_epic_key_to_jira_key.sql b/roadmap-planner/backend/internal/storage/migrations/0006_rename_epic_key_to_jira_key.sql new file mode 100644 index 00000000..1777115c --- /dev/null +++ b/roadmap-planner/backend/internal/storage/migrations/0006_rename_epic_key_to_jira_key.sql @@ -0,0 +1,22 @@ +-- ---------------------------------------------------------------------- +-- 0006_rename_epic_key_to_jira_key — W5 (2026-05-19). +-- +-- The column was misnamed at birth: the linker pulls *any* matching Jira +-- key out of the source branch / title regex, not specifically an Epic +-- key. In a 4411-PR-linked sample from prod, the distribution was: +-- Story 2817, Bug 996, Job 199, Technical Debt 195, Document 118, +-- Sub-task 57, Task 25, Improvement 4, **Epic 0**. +-- +-- Zero rows ever pointed at an actual Epic. The misnomer wasted future +-- reader time and was load-bearing in one place — `sprintCounts` joins +-- `pull_requests.epic_key = issue_snapshots.issue_key` and happened to +-- be correct only because most sprint members are Stories. +-- +-- This rename is internal — no API client reads the JSON tag (verified +-- by grep on `frontend/` + the docs/). Safe to flip in one migration. +-- ---------------------------------------------------------------------- + +ALTER TABLE pull_requests RENAME COLUMN epic_key TO jira_key; + +DROP INDEX IF EXISTS idx_pr_epic; +CREATE INDEX IF NOT EXISTS idx_pr_jira ON pull_requests(jira_key); diff --git a/roadmap-planner/backend/internal/storage/store.go b/roadmap-planner/backend/internal/storage/store.go index 13e63aaa..50cbd0d6 100644 --- a/roadmap-planner/backend/internal/storage/store.go +++ b/roadmap-planner/backend/internal/storage/store.go @@ -93,9 +93,12 @@ type IssueSnapshot struct { ResolvedAt *time.Time } -// PullRequest is a GitHub PR or GitLab MR record. Linked to an Epic via -// EpicKey when the configured Linker can resolve one (branch regex, -// title, etc.). +// PullRequest is a GitHub PR or GitLab MR record. Linked to a Jira +// issue via JiraKey when the configured Linker can resolve one +// (branch regex, title, etc.). JiraKey is *not* restricted to Epics — +// the regex matches any DEVOPS-NNN id, which in prod is mostly +// Stories / Bugs (see migration 0006 for the audit data behind the +// rename from `epic_key`). // // AuthorLogin is the raw login string returned by the source API (lower- // cased on write). It's stored alongside the resolved AuthorID so the @@ -120,7 +123,7 @@ type PullRequest struct { Additions int Deletions int ChangedFiles int - EpicKey string + JiraKey string // any Jira key matched by the linker; was misnamed `EpicKey` pre-W5 CreatedAt time.Time FirstReviewAt *time.Time MergedAt *time.Time diff --git a/roadmap-planner/backend/internal/storage/store_test.go b/roadmap-planner/backend/internal/storage/store_test.go index 6e5ffa00..c128a4f4 100644 --- a/roadmap-planner/backend/internal/storage/store_test.go +++ b/roadmap-planner/backend/internal/storage/store_test.go @@ -118,7 +118,7 @@ func TestSQLiteRoundTrip(t *testing.T) { Number: 173, Title: "ci: smoke test", State: "merged", AuthorID: "alice", HeadBranch: "DEVOPS-1-smoke", BaseBranch: "main", Additions: 42, Deletions: 8, ChangedFiles: 3, - EpicKey: "DEVOPS-1", + JiraKey: "DEVOPS-1", CreatedAt: now.Add(-12 * time.Hour), FirstReviewAt: &first, MergedAt: &merged, diff --git a/roadmap-planner/docs/team-analytics/CHANGES.md b/roadmap-planner/docs/team-analytics/CHANGES.md index 9ec1dfba..7aafebb0 100644 --- a/roadmap-planner/docs/team-analytics/CHANGES.md +++ b/roadmap-planner/docs/team-analytics/CHANGES.md @@ -8,6 +8,37 @@ should consult this when upgrading. Newest entries first. +## W5 — `pull_requests.epic_key` → `jira_key` rename + +**What changed** + +- Migration `0006_rename_epic_key_to_jira_key.sql` renames the column + via `ALTER TABLE pull_requests RENAME COLUMN epic_key TO jira_key` + (portable in both SQLite ≥3.25 and Postgres), drops the + `idx_pr_epic` index, and re-creates it as `idx_pr_jira`. +- Go struct field `PullRequest.EpicKey` → `PullRequest.JiraKey`, with + no alias (hard rename). +- Every SQL reference and assignment in the github / gitlab syncers, + the storage upsert, and the `sprintCounts` join updates in lock-step. + +**Why** + +The column was misnamed at birth: the linker pulls *any* Jira key out +of the source branch / title via `[A-Z]+-\d+`, so the value is mostly +a Story or Bug, never an Epic. Audit on prod (4411 linked PRs) showed +**zero** rows pointing at an actual Epic. The misnomer wasted reader +time and was load-bearing in one place — `sprintCounts` happened to be +correct only because most sprint members are Stories. + +**Backward compatibility** + +The JSON tag `epic_key` flips to `jira_key`. We grep'd `frontend/` and +no API consumer reads it (the PR list endpoint never exposed it; only +counts are surfaced). Safe one-shot rename. + +**Audit cross-reference:** `docs/team-analytics/audit-2026-05-19/REPORT.md` +finding B8. + ## W6 — GitLab parity: `hydrate_diff` default flips to `true` **What changed** @@ -44,37 +75,6 @@ merged MRs. **Audit cross-reference:** `docs/team-analytics/audit-2026-05-19/REPORT.md` finding B12. -## W5 — `pull_requests.epic_key` → `jira_key` rename - -**What changed** - -- Migration `0006_rename_epic_key_to_jira_key.sql` renames the column - via `ALTER TABLE pull_requests RENAME COLUMN epic_key TO jira_key` - (portable in both SQLite ≥3.25 and Postgres), drops the - `idx_pr_epic` index, and re-creates it as `idx_pr_jira`. -- Go struct field `PullRequest.EpicKey` → `PullRequest.JiraKey`, with - no alias (hard rename). -- Every SQL reference and assignment in the github / gitlab syncers, - the storage upsert, and the `sprintCounts` join updates in lock-step. - -**Why** - -The column was misnamed at birth: the linker pulls *any* Jira key out -of the source branch / title via `[A-Z]+-\d+`, so the value is mostly -a Story or Bug, never an Epic. Audit on prod (4411 linked PRs) showed -**zero** rows pointing at an actual Epic. The misnomer wasted reader -time and was load-bearing in one place — `sprintCounts` happened to be -correct only because most sprint members are Stories. - -**Backward compatibility** - -The JSON tag `epic_key` flips to `jira_key`. We grep'd `frontend/` and -no API consumer reads it (the PR list endpoint never exposed it; only -counts are surfaced). Safe one-shot rename. - -**Audit cross-reference:** `docs/team-analytics/audit-2026-05-19/REPORT.md` -finding B8. - ## W2 — Bot consolidation under one synthetic `bot` member **What changed**