Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 72 additions & 11 deletions platform/lib/queries/org-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,24 @@ export async function getOrgLatestPayloads(

export interface OrgContributorInfo {
count: number;
/** Map from lowercase name → { name, github? } for avatar lookup. */
/**
* Map from resolved identity → { name, github? }, for avatar lookup.
* Keyed by GitHub username (lowercased) when known — that's the stable
* identity `iris/cli.py`'s push-time resolution already computed — and
* only falls back to the lowercase display name when no github was
* resolved for that entry. Two name variants that both resolved to the
* same github collapse into one entry here.
*/
userMap: Map<string, { name: string; github?: string }>;
/**
* Map from every distinct lowercase display name seen → its resolved
* github username, NOT deduped by identity (multiple names can point at
* the same github). Lets a caller holding only a name (e.g. an
* author_velocity entry, which has no github field of its own) look up
* the identity that name resolved to, even when userMap's canonical
* entry for that identity settled on a different display name.
*/
nameToGithub: Map<string, string>;
}

export async function getOrgActiveContributors(
Expand All @@ -78,11 +94,14 @@ export async function getOrgActiveContributors(
.order("created_at", { ascending: false })
.limit(200);

if (!data || data.length === 0) return { count: 0, userMap: new Map() };
if (!data || data.length === 0) {
return { count: 0, userMap: new Map(), nameToGithub: new Map() };
}

// Keep only the latest run per repo
const seen = new Set<string>();
const userMap = new Map<string, { name: string; github?: string }>();
const nameToGithub = new Map<string, string>();

for (const row of data) {
if (seen.has(row.repository_id)) continue;
Expand All @@ -93,15 +112,18 @@ export async function getOrgActiveContributors(
>;
for (const u of users) {
const parsed = typeof u === "string" ? { name: u } : u;
const key = parsed.name.toLowerCase();
const key = parsed.github?.toLowerCase() ?? parsed.name.toLowerCase();
// Keep entry with github if available
if (!userMap.has(key) || (parsed.github && !userMap.get(key)?.github)) {
userMap.set(key, parsed);
}
if (parsed.github) {
nameToGithub.set(parsed.name.toLowerCase(), parsed.github);
}
}
}

return { count: userMap.size, userMap };
return { count: userMap.size, userMap, nameToGithub };
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1100,15 +1122,37 @@ export function isHyperEngineer(author: {
return author.high_velocity_weeks > 0 || author.ai_commit_pct >= 80;
}

/**
* Normalize a GitHub noreply commit email to its username, mirroring
* `_normalize_author` in `iris/analysis/author_velocity.py` — the same
* per-person key the engine already uses to dedupe an author's commits
* within one repo. Applying it here too means two name variants that
* share a noreply email (extremely common: it's stable per GitHub account
* regardless of local git config) collapse into one entry even when
* neither has a resolved `github` field.
*/
function normalizeEmailIdentity(email: string | undefined): string | null {
if (!email) return null;
const m = /^(?:\d+\+)?(.+)@users\.noreply\.github\.com$/i.exec(email.trim());
return (m ? m[1] : email.trim()).toLowerCase() || null;
}

export function computeHyperEngineers(
payloads: Map<string, ReportMetrics>,
userMap: Map<string, { name: string; github?: string }>,
nameToGithub: Map<string, string>,
): HyperEngineer[] {
// Accumulate per-author stats across repos
// Accumulate per-author stats across repos. Grouped by the most
// authoritative identity available, in order: resolved GitHub username
// (iris/cli.py already resolved this at push time, including real API
// lookups for non-noreply emails) -> normalized email -> display name as
// a last resort. Without this, the same person shows up once per distinct
// commit-author name they've ever used.
const authors = new Map<
string,
{
name: string;
names: Set<string>;
github?: string;
repos: number;
hvWeeks: number;
aiPct: number;
Expand All @@ -1121,16 +1165,25 @@ export function computeHyperEngineers(
const av = p.author_velocity;

for (const a of av.authors) {
const key = a.name.toLowerCase();
if (!isHyperEngineer(a)) continue;

const nameLower = a.name.toLowerCase();
const github = nameToGithub.get(nameLower);
const key = github ?? normalizeEmailIdentity(a.email) ?? nameLower;

const existing = authors.get(key) ?? {
name: a.name,
names: new Set<string>(),
github: undefined,
repos: 0,
hvWeeks: 0,
aiPct: 0,
aiCount: 0,
};
existing.names.add(a.name);
// Carry the resolved github forward directly — don't rely solely on
// userMap having a matching entry, since nameToGithub can know a
// mapping userMap's own dedup pass didn't happen to retain.
if (github) existing.github = github;
existing.repos++;
existing.hvWeeks = Math.max(existing.hvWeeks, a.high_velocity_weeks);
existing.aiPct += a.ai_commit_pct;
Expand All @@ -1141,10 +1194,18 @@ export function computeHyperEngineers(

return [...authors.entries()]
.map(([key, a]) => {
const userInfo = userMap.get(key);
// userMap is keyed by github-when-known, so this hits directly when
// `key` is already a github username. Otherwise, fall back to trying
// every name variant we saw for this person, in case userMap's own
// fallback (name-keyed) entry matches one of them.
const userInfo =
userMap.get(key) ??
[...a.names].map((n) => userMap.get(n.toLowerCase())).find(Boolean);
const displayName =
userInfo?.name ?? [...a.names].sort((x, y) => y.length - x.length)[0];
return {
name: userInfo?.name ?? a.name,
github: userInfo?.github,
name: displayName,
github: a.github ?? userInfo?.github,
repos: a.repos,
highVelocityWeeks: a.hvWeeks,
aiCommitPct: a.aiCount > 0 ? a.aiPct / a.aiCount : 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ export async function HyperEngineersPanel({

return (
<HyperEngineers
engineers={computeHyperEngineers(payloads, contributors.userMap)}
engineers={computeHyperEngineers(
payloads,
contributors.userMap,
contributors.nameToGithub,
)}
/>
);
}
110 changes: 110 additions & 0 deletions platform/tests/org-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
computeAIvsHuman,
computeDeliveryQuality,
computeHyperEngineers,
computeOrgPulse,
computePreviousTotals,
computePRHealth,
Expand Down Expand Up @@ -344,3 +345,112 @@ describe("isHyperEngineer — shared threshold", () => {
);
});
});

describe("computeHyperEngineers — dedupes the same person across name variants", () => {
function hyperAuthor(over: {
name: string;
email?: string;
high_velocity_weeks?: number;
}) {
return {
name: over.name,
email: over.email,
high_velocity_weeks: over.high_velocity_weeks ?? 1,
ai_commit_pct: 50,
};
}

it("merges two display-name variants that resolved to the same github username", () => {
const payloads = new Map<string, ReportMetrics>([
[
"repo-a",
payload({
author_velocity: {
authors: [hyperAuthor({ name: "Renato Guimaraes" })],
},
}),
],
[
"repo-b",
payload({
author_velocity: {
authors: [hyperAuthor({ name: "Renato Guimarães (Bahia)" })],
},
}),
],
]);
const nameToGithub = new Map([
["renato guimaraes", "renatoguimaraescb"],
["renato guimarães (bahia)", "renatoguimaraescb"],
]);

const result = computeHyperEngineers(payloads, new Map(), nameToGithub);

expect(result).toHaveLength(1);
expect(result[0].github).toBe("renatoguimaraescb");
expect(result[0].repos).toBe(2);
});

it("merges two display-name variants sharing a GitHub noreply email", () => {
const payloads = new Map<string, ReportMetrics>([
[
"repo-a",
payload({
author_velocity: {
authors: [
hyperAuthor({
name: "Renato Guimaraes",
email: "123+renatoguimaraescb@users.noreply.github.com",
}),
],
},
}),
],
[
"repo-b",
payload({
author_velocity: {
authors: [
hyperAuthor({
name: "renatoguimaraescb",
email: "123+renatoguimaraescb@users.noreply.github.com",
}),
],
},
}),
],
]);

// No github resolved anywhere — only the shared noreply email ties
// the two name variants together.
const result = computeHyperEngineers(payloads, new Map(), new Map());

expect(result).toHaveLength(1);
expect(result[0].repos).toBe(2);
});

it("keeps genuinely different people separate", () => {
const payloads = new Map<string, ReportMetrics>([
[
"repo-a",
payload({
author_velocity: {
authors: [hyperAuthor({ name: "Alice", email: "alice@corp.com" })],
},
}),
],
[
"repo-b",
payload({
author_velocity: {
authors: [hyperAuthor({ name: "Bob", email: "bob@corp.com" })],
},
}),
],
]);

const result = computeHyperEngineers(payloads, new Map(), new Map());

expect(result).toHaveLength(2);
});
});
Loading