feat(team-summary): team comparison view with three impact strategies (GLOOK-4)#42
Conversation
Captures the design for the new Teams tab on /report/[id]/team that compares teams instead of individual contributors. Key decisions: - Primary impact metric (W) = per-capita-then-apply with authoritative team size from team_members (not "active devs that shipped this period"). Same IC impact formula reused via an extracted shared module. - Two secondary impact columns retained: (A) arithmetic mean of active devs' impacts, (T) sum-then-apply. - All columns sortable, default sort by W desc. - Tab strip on the existing page, URL state ?view=teams. Default remains ?view=individuals so existing bookmarks are unaffected. - Row click drills back to Individuals filtered by that team via the existing team-filter URL state. - Aggregation runs client-side via a pure aggregateTeams() function; no new API endpoint. Also adds scripts/team-impact-poc.ts — the one-off used during brainstorming to compare W, A, T against real data from the latest report. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…mplementation Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
10 bite-sized tasks: extract impact-score module (Task 1), build aggregateTeams via TDD (Tasks 2-7), TeamTable component (Task 8), wire Teams tab + URL state into team/page.tsx (Task 9), local smoke (Task 10). Co-Authored-By: Claude <noreply@anthropic.com>
…e (GLOOK-4) Same function, new home. Server-side aggregator continues to import it through src/lib/aggregator.ts (re-export), so no callers change. Adds a focused test file that pins the formula's numeric behavior, which the upcoming team aggregator will rely on. Co-Authored-By: Claude <noreply@anthropic.com>
Empty implementation, ready for incremental TDD in the next tasks. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…a (GLOOK-4) Co-Authored-By: Claude <noreply@anthropic.com>
…K-4) Co-Authored-By: Claude <noreply@anthropic.com>
…ill-down (GLOOK-4) Co-Authored-By: Claude <noreply@anthropic.com>
URL-stated ?view=individuals|teams, defaults to individuals. Existing filter dropdown, dev-search, and TeamPulseCard render only on the individuals tab. Co-Authored-By: Claude <noreply@anthropic.com>
Smoke-test feedback on Task 10: - Sort state is now stored via useUrlState (?sort=…&dir=…) so a hard reload keeps the active sort column and direction. - Row click now also sets the dev URL state (?dev=login1&dev=login2…) using the authoritative team_members list, mirroring what the in-page team-filter dropdown does. The IC tab now drills in with both the team-pulse selection AND the dev-filter chips populated, matching existing muscle memory. Co-Authored-By: Claude <noreply@anthropic.com>
…ecimals (GLOOK-4) cc_total_cost is stored in cents (decimal(10,2)); the IC table already renders it as $(value / 100).toFixed(2). Team totals were 100× too high and overly precise. Switch to Math.round(value / 100).toLocaleString() to match: whole dollars, thousands separators, matching green-mono styling. Co-Authored-By: Claude <noreply@anthropic.com>
msogin
left a comment
There was a problem hiding this comment.
Review findings — 4 suggested fixes, 1 question
Reviewed production code only. Tests pass, TypeScript clean, architecture is sound. Four issues worth addressing before merge:
I1 — teams as AggregatorTeam[] cast bypasses TypeScript
File: src/app/report/[id]/team/page.tsx
The local Team interface ({ id, name, members }) is missing color: string, which AggregatorTeam requires. The cast papers over the gap. Fix: add color: string to the local Team interface, remove the cast.
I3 — Teams SWR waterfall renders false "No teams configured" during load
File: src/app/report/[id]/team/page.tsx
The teams SWR key depends on org from the first SWR. During the window between activeReport resolving and teamsData resolving, teams=[] so aggregateTeams returns [] and the empty-state message fires incorrectly.
Fix: change the TeamTable render condition from:
{view === "teams" && activeReport && ...}
to:
{view === "teams" && activeReport && teamsData !== undefined && ...}
No loading placeholder needed — this is a fast sequential same-origin fetch.
S1 — cc_total_cost in SORT_KEYS but column hidden for non-admins
File: src/app/report/[id]/team/team-table.tsx
A non-admin opening a shared link with ?sort=cc_total_cost sees the table sorted by an invisible column with no sort caret.
Fix: const effectiveSortKey = (!hasSpend && sortKey === "cc_total_cost") ? "impact_weighted" : sortKey; — use effectiveSortKey in sortedRows useMemo and sortCaret.
S2 — Lines +/- column missing toLocaleString
File: src/app/report/[id]/team/team-table.tsx
IC table uses toLocaleString() for line counts; team table does not. On high-volume orgs team totals exceed 50k.
Fix: row.lines_added.toLocaleString() and row.lines_removed.toLocaleString()
Question (S8)
team-aggregator.ts: If team.members contains a duplicate login, devByLogin.get deduplicates silently but team.members.length (used as teamSize) is inflated, deflating impact_weighted without any signal. Is member-list uniqueness guaranteed by the server?
Phase 1 flags (non-blocking)
- scripts/team-impact-poc.ts (163 lines): PR description calls it "one-off used during brainstorming." Consider whether it belongs merged to main.
- docs/superpowers/plans + docs/superpowers/specs (1290 lines combined): planning artifacts that go stale post-merge. The PR description already captures the rationale — consider Confluence/Jira instead of source control.
msogin
left a comment
There was a problem hiding this comment.
Review findings for PR 42 - see summary below
Review findings — 4 fixes, 1 questionReviewed production code only (, , , , ). Tests pass, TypeScript clean, architecture sound. Four issues before merge: I1 —
|
I1 — local Team interface was missing `color: string`, papered over by a `teams as AggregatorTeam[]` cast. The /api/teams response already includes color, so the local type just needed widening. Cast and now- unused import removed. I3 — TeamTable rendered before the /api/teams SWR resolved, so a brief window during initial load with `teams=[]` triggered the false "No teams configured" empty state. Gate on `teamsData !== undefined`. S1 — sort URL state could point at a column hidden for non-admins (Spend) or for orgs without Jira issues. Render then showed a sort with no caret indicator. Add an `effectiveSortKey` fallback to `impact_weighted` when the URL-specified column is invisible; this fallback is used by both the sort comparator and the caret. S2 — Lines +/- column showed bare integers; team totals can run into the tens of thousands. Switch to `.toLocaleString()` to match the IC table's formatting. S8 — Not a code change. Reviewer asked whether `team.members` could contain duplicates (which would inflate teamSize and deflate impact_weighted). DB has UNIQUE KEY (team_id, github_login) on team_members, so duplicates are structurally impossible. Co-Authored-By: Claude <noreply@anthropic.com>
Summary
Implements GLOOK-4: a new Teams tab on
/report/[id]/teamthat compares teams against each other using the same impact dimensions as the IC table. Adds three impact columns (Weighted / Avg / Total) and full column parity with the Individuals table.Spec:
docs/superpowers/specs/2026-05-19-glook-4-team-comparison-design.mdPlan:
docs/superpowers/plans/2026-05-19-glook-4-team-comparison.mdImpact formulas
For each team, three numbers — all derived from the existing IC
computeImpactScoreformula, no new weights:team_members, then run the IC formula. Inactive members still dilute per-capita — by design.min(x/N, 1)caps engage at single-developer scale).Brainstormed against real data from your latest report —
scripts/team-impact-poc.tswas used to compare strategies side-by-side on real team rollups. (W) was the most defensible primary sort: it differentiates between teams without inflating large teams or unfairly rewarding small teams.Architecture
/api/report/[id](devs) and/api/teams?org=<org>(team members). A new pure functionaggregateTeams(developers, teams) → TeamRow[]runs inside auseMemoand feeds a new<TeamTable />component. No new API endpoint, no DB migration.computeImpactScoreextracted tosrc/lib/impact-score.tsso server (per-dev impact at report run time) and client (team-level impact) call the same function. No drift.UI
/report/[id]/org. URL state?view=individuals | teams, defaultindividuals— existing bookmarks unaffected.−Nindicator when inactive), PRs, Commits, Lines +/−, Cmplx, PR%, AI%, Jira (conditional), Spend (admin-only, conditional), Impact W/A/T.?sort=…&dir=…) so it survives page reload and shareable links.team(drives TeamPulseCard) anddev(drives the dev-filter chips and table) URL state, mirroring the existing dropdown behavior.Files
src/lib/impact-score.ts— extractedcomputeImpactScore(function move, no logic change).src/lib/teams/team-aggregator.ts—aggregateTeams(developers, teams)pure function.src/app/report/[id]/team/team-table.tsx—<TeamTable />component.src/lib/__tests__/unit/impact-score.test.ts— pinned numeric assertions on the extracted formula.src/lib/__tests__/unit/team-aggregator.test.ts— 15 tests covering sum aggregation, weighted ratios, type/repo aggregations, the three impact strategies, single-member team invariant (W = T), multi-team membership, no-team devs, orphan team skip.scripts/team-impact-poc.ts— one-off used during brainstorming to compare formulas against real data; useful for future formula tweaks.docs/superpowers/specs/2026-05-19-glook-4-team-comparison-design.md.docs/superpowers/plans/2026-05-19-glook-4-team-comparison.md.src/lib/aggregator.ts— re-exportcomputeImpactScorefrom the new module so existing callers stay unchanged.src/app/report/[id]/team/page.tsx—?view=URL state, tab strip, conditional render, hide filter dropdown + dev-search + TeamPulseCard on Teams view.Tests
npm test→ 62 suites / 647 tests pass.npx tsc --noEmit -p tsconfig.json→ clean.impact-score+ 15 onteam-aggregator). UI smoke happened locally per the plan's Task 10 checklist.Known caveats (documented in spec)
impact_scorewas computed withtotalStoryPointswhich isn't exposed via/api/report. Tolerable for v1; smallest follow-up fix is addingtotal_story_pointsto the report SELECT.Test plan
/report/{id}/team— defaults to Individuals tab, no behavior change.?view=teams&sort=total_commits&dir=desc— sort preserved.?view=individuals&team=<name>&dev=<login>&dev=<login>…; IC view filters to those members; TeamPulseCard appears.N −Min Active column.🤖 Generated with Claude Code