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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 29 additions & 14 deletions crates/pulse-core/src/views/journey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
77 changes: 75 additions & 2 deletions crates/pulse/tests/snapshot_views.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down Expand Up @@ -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();
Expand Down
Loading