feat: source-priority tiebreaker in affiliation timeline builder (CM-1106)#4055
feat: source-priority tiebreaker in affiliation timeline builder (CM-1106)#4055
Conversation
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds source-aware tie-breaking to the affiliation timeline builder’s primary work-experience selection so that overlapping dated work experiences prefer higher-trust sources (ui > email-domain > enrichment-* > others) before existing heuristics.
Changes:
- Introduces
getMemberOrganizationSourceRankin@crowd/commonto rank member-organization sources. - Applies source-tier tie-breaking in
selectPrimaryWorkExperiencewhen multiple dated rows overlap. - Includes
mo."source"in the memberOrganizations timeline query so source-based selection is possible.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| services/libs/data-access-layer/src/member-organization-affiliation/index.ts | Adds source-tier tie-break step and selects source from DB for timeline building. |
| services/libs/common/src/member.ts | Adds a shared helper for ranking member-organization sources. |
Comments suppressed due to low confidence (1)
services/libs/data-access-layer/src/member-organization-affiliation/index.ts:99
memberOrgsOnlyis filtered by'segmentId' in row, butsegmentIdis what distinguishes manual affiliations (IManualAffiliationData). In this branch manual affiliations have already been returned early, so this filter will always be empty and the memberCount tiebreaker will never run. Filter for actual member-organization rows instead (e.g., exclude items withsegmentId, or use a type guard onMemberOrganizationWithOverrides).
// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
) as MemberOrganizationWithOverrides[]
if (memberOrgsOnly.length >= 2) {
const sortedByMembers = memberOrgsOnly.sort((a, b) => b.memberCount - a.memberCount)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 25bcf9a. Configure here.
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
services/libs/data-access-layer/src/member-organization-affiliation/index.ts:97
memberOrgsOnlyis intended to exclude manual affiliations (which don’t havememberCount), but the current predicate ('segmentId' in row && !!row.segmentId) actually selects manual affiliations. SinceMemberOrganizationWithOverrides(IMemberOrganization) doesn’t havesegmentId, this filter will usually return an empty array and the member-count tiebreaker will never run. Consider filtering by the absence ofsegmentId(or using a proper type guard) so member orgs are compared bymemberCountas intended.
// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
) as MemberOrganizationWithOverrides[]
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
services/libs/data-access-layer/src/member-organization-affiliation/index.ts:98
- The
memberOrgsOnlyfilter is inverted: it selects rows that havesegmentId(manual affiliations), but this branch only runs when there are no manual affiliations, somemberOrgsOnlywill always be empty and the memberCount tiebreaker never executes. Filter for member-organization rows instead (e.g.,!('segmentId' in row)or'memberCount' in row) so memberCount comparison works as intended.
// 3. get the two orgs with the most members, and return the one with the most members if there's no draw
// only compare member orgs (manual affiliations don't have memberCount)
const memberOrgsOnly = orgs.filter(
(row: AffiliationItem) => 'segmentId' in row && !!row.segmentId,
) as MemberOrganizationWithOverrides[]
if (memberOrgsOnly.length >= 2) {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const sourceRank = (row: AffiliationItem) => | ||
| getMemberOrganizationSourceRank((row as MemberOrganizationWithOverrides).source) | ||
| const bestRank = Math.min(...withDates.map(sourceRank)) | ||
| orgs = withDates.filter((row) => sourceRank(row) === bestRank) | ||
| if (orgs.length === 1) return orgs[0] |
There was a problem hiding this comment.
sourceRank relies on an unchecked cast to MemberOrganizationWithOverrides to access .source. Since AffiliationItem is a union, this hides type issues and can mask undefined at runtime. Prefer a type guard (e.g. check 'source' in row) or narrow to member-organization rows before ranking sources.
| const bestRank = Math.min(...withDates.map((r) => getMemberOrganizationSourceRank(r.source))) | ||
| const topSourceGroup = withDates.filter( | ||
| (r) => getMemberOrganizationSourceRank(r.source) === bestRank, | ||
| ) |
There was a problem hiding this comment.
This computes getMemberOrganizationSourceRank(r.source) twice per row (once for bestRank, once for filtering). Consider computing ranks once (e.g., map to {row, rank}) to avoid duplicated work and guarantee consistent ranking if the function changes.
| const bestRank = Math.min(...withDates.map((r) => getMemberOrganizationSourceRank(r.source))) | |
| const topSourceGroup = withDates.filter( | |
| (r) => getMemberOrganizationSourceRank(r.source) === bestRank, | |
| ) | |
| const rankedWithDates = withDates.map((row) => ({ | |
| row, | |
| rank: getMemberOrganizationSourceRank(row.source), | |
| })) | |
| const bestRank = Math.min(...rankedWithDates.map(({ rank }) => rank)) | |
| const topSourceGroup = rankedWithDates | |
| .filter(({ rank }) => rank === bestRank) | |
| .map(({ row }) => row) |

Adds source-aware tiebreaking to
selectPrimaryWorkExperiencein both the affiliation timeline builder and the public API path.When multiple dated
memberOrganizationsrows cover the same day, the winner is now decided by source tier before falling through to the existing member-count and date-range heuristics:ui → email-domain → enrichment-* → anything else
Source priority is only applied to dated rows, so undated rows remain last-resort regardless of source.
Note
Medium Risk
Changes the tie-breaking logic used to pick a primary affiliation when multiple dated organizations overlap, which can alter computed timelines and downstream affiliation-driven behavior. Risk is moderate because it affects core resolution logic but is bounded by explicit source tiers and added test coverage.
Overview
Adds a source-aware tiebreaker when choosing the “winning” organization for overlapping dated work experiences, prioritizing
uioveremail-domainoverenrichment-*before falling back to member-count and date-range heuristics.To support this, the API affiliation query now includes
memberOrganizations.source, a shared helper (getMemberOrganizationSourceRank) is introduced in@crowd/common, and tests are extended to cover the new source-priority cases (including ensuring undated rows are unaffected).Reviewed by Cursor Bugbot for commit 2004aa2. Bugbot is set up for automated code reviews on this repo. Configure here.