diff --git a/platform/lib/queries/org-summary.ts b/platform/lib/queries/org-summary.ts index c52e17b..e7a5789 100644 --- a/platform/lib/queries/org-summary.ts +++ b/platform/lib/queries/org-summary.ts @@ -1160,6 +1160,26 @@ export function computeHyperEngineers( } >(); + // Learn email -> github wherever a name-based lookup resolves one, before + // grouping. This is the same "known mapping learned elsewhere" trick as + // the nameToGithub/userMap checks below, just keyed by raw commit email + // instead of name: a person can show up "identified" in one repo (their + // email there matched a name we could resolve) and "unidentified" in + // another (same actual email, but paired with a name variant that never + // resolved) — this bridges the second occurrence to the first without + // requiring push-time API resolution to have succeeded for every repo. + const emailToGithub = new Map(); + for (const [, p] of payloads) { + if (!p.author_velocity?.authors) continue; + for (const a of p.author_velocity.authors) { + if (!isHyperEngineer(a) || !a.email) continue; + const nameLower = a.name.toLowerCase(); + const github = + nameToGithub.get(nameLower) ?? userMap.get(nameLower)?.github; + if (github) emailToGithub.set(a.email.toLowerCase(), github); + } + } + for (const [repoId, p] of payloads) { if (!p.author_velocity?.authors) continue; const av = p.author_velocity; @@ -1168,6 +1188,7 @@ export function computeHyperEngineers( if (!isHyperEngineer(a)) continue; const nameLower = a.name.toLowerCase(); + const emailLower = a.email?.toLowerCase(); // nameToGithub alone misses a real case: some people's local git // config uses their GitHub handle itself as the commit author name // (e.g. author name "lucastribioliclickbus" instead of "Lucas @@ -1175,8 +1196,12 @@ export function computeHyperEngineers( // that name's email to a login. userMap is *also* keyed by github // username when known, so checking it here — not just at display // time — catches that case during grouping instead of after. + // emailToGithub is the last resort: the same raw commit email tied to + // a resolved identity somewhere else entirely, regardless of name. const github = - nameToGithub.get(nameLower) ?? userMap.get(nameLower)?.github; + nameToGithub.get(nameLower) ?? + userMap.get(nameLower)?.github ?? + (emailLower ? emailToGithub.get(emailLower) : undefined); const key = github ?? normalizeEmailIdentity(a.email) ?? nameLower; const existing = authors.get(key) ?? { diff --git a/platform/tests/org-summary.test.ts b/platform/tests/org-summary.test.ts index 0524efa..ac240a4 100644 --- a/platform/tests/org-summary.test.ts +++ b/platform/tests/org-summary.test.ts @@ -555,6 +555,44 @@ describe("computeHyperEngineers — dedupes the same person across name variants expect(result[0].repos).toBe(1); }); + it("merges an 'unidentified' entry into an 'identified' one via a shared raw email, when neither name nor userMap resolves it directly", () => { + // Same person, two repos: repo-a's commit name resolves to a github via + // nameToGithub (this repo would render "identified"). repo-b uses a + // completely different name string (e.g. a nickname) with the SAME raw + // email — neither nameToGithub nor userMap has an entry for that name, + // so without the email bridge this would render as a second, + // "unidentified" card for the same real person. + const payloads = new Map([ + [ + "repo-a", + payload({ + author_velocity: { + authors: [ + hyperAuthor({ name: "Carla Souza", email: "carla@corp.com" }), + ], + }, + }), + ], + [ + "repo-b", + payload({ + author_velocity: { + authors: [ + hyperAuthor({ name: "carlinha", email: "carla@corp.com" }), + ], + }, + }), + ], + ]); + const nameToGithub = new Map([["carla souza", "carla-gh"]]); + + const result = computeHyperEngineers(payloads, new Map(), nameToGithub); + + expect(result).toHaveLength(1); + expect(result[0].github).toBe("carla-gh"); + expect(result[0].repos).toBe(2); + }); + it("still includes an engineer with no resolved GitHub username, with github left undefined", () => { // computeHyperEngineers doesn't filter these out — the "identified" vs // "unidentified" split is a display concern, done in HyperEngineers.tsx.