From 9ba075d12d4ed690243f9caf503930aaba58d753 Mon Sep 17 00:00:00 2001 From: Sergey Vilgelm Date: Mon, 2 Nov 2026 10:26:33 -0800 Subject: [PATCH] Follow company credit in Journey affiliation entries Entries mark the first contribution credited to each company, including moves to an undated company, instead of the configured period start. Assisted-By: Claude Opus 5.5 Signed-off-by: Sergey Vilgelm --- README.md | 6 +- crates/pulse-core/src/views/journey.rs | 43 +++++++++----- crates/pulse/tests/snapshot_views.rs | 77 +++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3857230..eb27290 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ day from public GitHub activity. activity, company affiliations, reviews, AI usage, pull requests, and commits. - **Journey**: the second tab of a contributor's page: a timeline from their first contribution to their latest activity (first merged pull request, 100 merged pull - requests, first contribution to each repository, anniversaries, affiliation changes) - and achievements with the date each was unlocked. Each achievement has a button that - copies a link to the page with that badge highlighted. + requests, first contribution to each repository, anniversaries, first contribution + from each company) and achievements with the date each was unlocked. Each achievement + has a button that copies a link to the page with that badge highlighted. - **Companies**: contributions credited to each company and the people affiliated with it. - **AI Activity**: AI authorship and assistance found in commit trailers and AI diff --git a/crates/pulse-core/src/views/journey.rs b/crates/pulse-core/src/views/journey.rs index efc6e81..b9f613d 100644 --- a/crates/pulse-core/src/views/journey.rs +++ b/crates/pulse-core/src/views/journey.rs @@ -459,21 +459,36 @@ impl View<'_> { snapshot.companies.iter().map(|c| (c.id.as_str(), c)).collect(); let affiliation_history = profile_affiliations(&person.affiliation, &person.affiliations, &companies, &format_date(today)); - for row in &affiliation_history { - let name = row["company_name"].as_str().unwrap_or_default(); - let independent = row["company_id"] == "independent"; - // The start of each credited period; its end is not an event, since the - // dates bound the credited contributions, not the person's employment. - if let Some(day) = row["valid_from"].as_str().and_then(crate::time::parse_date).filter(|&d| d <= today) { - let event = if independent { - Event::new(day, 7, "affiliation", "Independent contributor") - .detail("Contributions from this day are not credited to a company") - } else { - Event::new(day, 7, "affiliation", format!("Contributing from {name}")) - .detail(format!("Contributions from this day are credited to {name}")) - }; - events.push(event); + // Entries follow company credit: a dated period covers its days, the undated company + // covers the others, and no match means Independent. Only people with dated periods + // change company, and starting out without one is not an event. + let date = |row: &Value, field: &str| row[field].as_str().and_then(crate::time::parse_date); + let undated = + affiliation_history.iter().find(|row| row["source"] == "CURATED" && date(row, "valid_from").is_none()); + let periods: Vec<_> = affiliation_history + .iter() + .filter_map(|row| Some((date(row, "valid_from")?, date(row, "valid_until"), row))) + .collect(); + let mut credited = None; + for &day in active_days.iter().filter(|_| !periods.is_empty()) { + let row = periods + .iter() + .find(|(from, until, _)| *from <= day && until.is_none_or(|until| day < until)) + .map(|(_, _, row)| *row) + .or(undated); + let company = row.map_or("independent", |row| row["company_id"].as_str().unwrap_or_default()); + if credited == Some(company) || (credited.is_none() && row.is_none()) { + continue; } + credited = Some(company); + events.push(if company == "independent" { + Event::new(day, 7, "affiliation", "Independent contributor") + .detail("Contributions from this day are not credited to a company") + } else { + let name = row.and_then(|row| row["company_name"].as_str()).unwrap_or_default(); + Event::new(day, 7, "affiliation", format!("Contributing from {name}")) + .detail(format!("Contributions from this day are credited to {name}")) + }); } // Anniversaries and tenure. diff --git a/crates/pulse/tests/snapshot_views.rs b/crates/pulse/tests/snapshot_views.rs index e914480..1e3ffdf 100644 --- a/crates/pulse/tests/snapshot_views.rs +++ b/crates/pulse/tests/snapshot_views.rs @@ -141,9 +141,16 @@ fn dataset() -> Dataset { dataset_of(state()) } +fn fixtures() -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures") +} + fn dataset_of(state: State) -> Dataset { - let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); - let companies = load_company_catalog(&config, &state.contributors).unwrap(); + dataset_with_config(state, &fixtures()) +} + +fn dataset_with_config(state: State, config: &Path) -> Dataset { + let companies = load_company_catalog(config, &state.contributors).unwrap(); let snapshot = build_snapshot(&state, &companies, parse_timestamp("2026-10-10T12:00:00Z").unwrap()); Dataset::new(Snapshot::decode(&snapshot.encode().unwrap()).unwrap()) } @@ -381,6 +388,72 @@ fn page_paths_and_stable_ids() { assert_eq!(companies["filters"]["repository"], "github/flyteorg/flyte"); } +#[test] +fn journey_dates_affiliations_by_first_contribution() { + let config = tempfile::tempdir().unwrap(); + let companies = std::fs::read_to_string(fixtures().join("companies.yaml")).unwrap(); + std::fs::write( + config.path().join("companies.yaml"), + format!("{companies}- id: anyscale\n canonical_name: Anyscale\n website_url: https://anyscale.com\n"), + ) + .unwrap(); + // Alice is active on October 1, 2, 3, and 5. + let union = "Contributing from Union"; + let anyscale = "Contributing from Anyscale"; + let independent = "Independent contributor"; + let cases: [(&str, &[(&str, &str)]); 6] = [ + // The first period has no contributions, and the others start before their first one. + ( + " - {company: union, from: '2026-09-01', until: '2026-09-15'} + - {company: independent, from: '2026-09-15', until: '2026-10-04'} + - {company: union, from: '2026-10-04'} +", + &[("2026-10-01", independent), ("2026-10-05", union)], + ), + // The undated company covers the days around a dated period. + ( + " - {company: anyscale} + - {company: union, from: '2026-09-01', until: '2026-10-03'} +", + &[("2026-10-01", union), ("2026-10-03", anyscale)], + ), + ( + " - {company: anyscale} + - {company: union, from: '2026-10-02', until: '2026-10-03'} +", + &[("2026-10-01", anyscale), ("2026-10-02", union), ("2026-10-03", anyscale)], + ), + // No company covers the other days; only leaving one is an event. + ( + " - {company: union, from: '2026-10-02', until: '2026-10-03'}\n", + &[("2026-10-02", union), ("2026-10-03", independent)], + ), + // Adjoining periods at one company are not a move. + ( + " - {company: union, from: '2026-09-01', until: '2026-10-02'} + - {company: union, from: '2026-10-02'} +", + &[("2026-10-01", union)], + ), + // One undated company never changes. + (" - {company: anyscale}\n", &[]), + ]; + for (companies, expected) in cases { + let affiliations = format!("affiliations:\n- username: alice\n companies:\n{companies}"); + std::fs::write(config.path().join("affiliations.yaml"), affiliations).unwrap(); + let data = dataset_with_config(state(), config.path()); + let journey = get(&data, "/api/users/github/alice/journey"); + let entries: Vec<(&str, &str)> = journey["timeline"] + .as_array() + .unwrap() + .iter() + .filter(|e| e["kind"] == "affiliation") + .map(|e| (e["date"].as_str().unwrap(), e["title"].as_str().unwrap())) + .collect(); + assert_eq!(entries, expected, "{companies}"); + } +} + #[test] fn contributor_journey() { let data = dataset();