Skip to content

Commit 1ec9f78

Browse files
authored
feat: health Score v2 (Akrites) scoring pipeline, lifecycle, and impact score (CM-IN-1196) (#4394)
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
1 parent 02b91b9 commit 1ec9f78

21 files changed

Lines changed: 1279 additions & 18 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
DESCRIPTION >
2+
- `collection_contributor_dependency_copy_ds` contains precomputed bus-factor (contributor
3+
dependency) results for the DEFAULT, unfiltered view of every collection - i.e. the exact
4+
output `collection_contributor_dependency.pipe` would compute today with no `repos`/`startDate`/
5+
`endDate`/`platform`/`activity_type`/`includeCollaborations` override and `limit=100`.
6+
- Created via `collection_contributor_dependency_copy.pipe` (`TYPE COPY`, daily schedule) to
7+
avoid recomputing the same expensive per-member GROUP BY over the full activity set on every
8+
request - see that pipe's DESCRIPTION for the full rationale (this materialization exists
9+
because the live computation triggered a Tinybird production rate-limit/concurrency alert on
10+
`lfx_insights` from timeouts under load).
11+
- One row per (collectionSlug, memberId) that made it into that collection's top-100 ranked
12+
contributors, already carrying the running-total percentage and the 51% cutoff membership flag
13+
so the serving pipe only needs a cheap point lookup, not a recompute.
14+
- `collectionSlug` is the collection this row belongs to.
15+
- `id` is the member id (contributor).
16+
- `displayName` is the member's display name.
17+
- `githubHandleArray` is the member's GitHub usernames.
18+
- `contributionCount` is the member's raw contribution count within the collection (all-time,
19+
all-repos, code contributions only - the default filter set).
20+
- `contributionPercentage` is `contributionCount` as a percentage of the collection's total
21+
contribution count.
22+
- `roles` is the member's maintainer roles within the collection, if any.
23+
- `contributionPercentageRunningTotal` is the cumulative percentage when contributors are
24+
ordered by `contributionPercentage DESC, id` - matches the live pipe's window function output.
25+
- `rank` is the 1-indexed rank of this contributor within the collection (1 = highest
26+
contributionCount), preserved so the serving pipe can `ORDER BY rank` cheaply instead of
27+
re-sorting by percentage.
28+
- `totalContributorCount` is the collection's total unique contributor count (same value
29+
repeated on every row for the collection) - matches `active_contributors.contributorCount` in
30+
the live pipe's response.
31+
- `isBusFactorCutoff` marks the rows the live pipe's WHERE clause would return (running total
32+
<= 51% OR running total minus this row's own percentage < 51%) - the serving pipe filters on
33+
this instead of recomputing the cutoff logic.
34+
- `updatedAt` is when this row's collection was last (re)computed - lets the serving pipe or an
35+
operator sanity-check freshness independent of the COPY job's own execution log.
36+
37+
SCHEMA >
38+
`collectionSlug` String,
39+
`id` String,
40+
`displayName` String,
41+
`githubHandleArray` Array(String),
42+
`contributionCount` UInt64,
43+
`contributionPercentage` Float64,
44+
`roles` Array(String),
45+
`contributionPercentageRunningTotal` Float64,
46+
`rank` UInt32,
47+
`totalContributorCount` UInt64,
48+
`isBusFactorCutoff` UInt8,
49+
`updatedAt` DateTime64(3)
50+
51+
ENGINE ReplacingMergeTree
52+
ENGINE_SORTING_KEY collectionSlug, rank
53+
ENGINE_VER updatedAt
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DESCRIPTION >
2+
- `health_score_v2_development_ds` holds the per-repo Development Activity category score (0-25)
3+
for Health Score v2. Populated by `health_score_v2_development.pipe`.
4+
- `repoUrl` is the repository URL — the join key back to `repositories`.
5+
- `developmentActivityScoreV2` — NULL when covered sub-signal weight is <40% of the 25pt max
6+
(spec Layer 1 graceful degradation), otherwise the rescaled 0-25 score.
7+
8+
SCHEMA >
9+
`repoUrl` String,
10+
`developmentActivityScoreV2` Nullable(UInt8)
11+
12+
ENGINE MergeTree
13+
ENGINE_SORTING_KEY repoUrl
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DESCRIPTION >
2+
- `health_score_v2_impact_ds` holds the per-repo raw Impact Score (0-100 float, pre-rounding)
3+
for Health Score v2. Populated by `health_score_v2_impact.pipe`.
4+
- `repoUrl` is the repository URL — the join key back to `repositories`.
5+
- `impactScoreRaw` — MAX(packages.impact) * 100 over packages published by this repo. NULL when
6+
the repo has no linked packages with a known impact value.
7+
8+
SCHEMA >
9+
`repoUrl` String,
10+
`impactScoreRaw` Nullable(Float64)
11+
12+
ENGINE MergeTree
13+
ENGINE_SORTING_KEY repoUrl
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
DESCRIPTION >
2+
- `health_score_v2_lifecycle_ds` holds the per-repo Lifecycle state for Health Score v2.
3+
Populated by `health_score_v2_lifecycle.pipe`.
4+
- `repoUrl` is the repository URL — the join key back to `repositories`.
5+
- `lifecycleLabelV2` — one of active/stable/declining/abandoned/archived, or NULL when the
6+
repo has no usable activity signal at all (2026-07-24, IN-1196 — see
7+
health_score_v2_lifecycle.pipe for the exact condition).
8+
9+
SCHEMA >
10+
`repoUrl` String,
11+
`lifecycleLabelV2` Nullable(String)
12+
13+
ENGINE MergeTree
14+
ENGINE_SORTING_KEY repoUrl
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DESCRIPTION >
2+
- `health_score_v2_maintainer_ds` holds the per-repo Maintainer Health category score (0-40)
3+
for Health Score v2. Populated by `health_score_v2_maintainer.pipe`.
4+
- `repoUrl` is the repository URL — the join key back to `repositories`.
5+
- `maintainerHealthScoreV2` — NULL when covered sub-signal weight is <40% of the 40pt max
6+
(spec Layer 1 graceful degradation), otherwise the rescaled 0-40 score.
7+
8+
SCHEMA >
9+
`repoUrl` String,
10+
`maintainerHealthScoreV2` Nullable(UInt8)
11+
12+
ENGINE MergeTree
13+
ENGINE_SORTING_KEY repoUrl
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
DESCRIPTION >
2+
- `health_score_v2_repo_copy_ds` contains the per-repo Akrites Health Score v2 breakdown.
3+
- Populated by `health_score_v2.pipe` copy pipe.
4+
- Health Score v2 is computed entirely from repo-level/CDP data (maintainer activity, security posture,
5+
development activity) — no package data, per the Health Score v2 methodology
6+
(https://linuxfoundation-dxwx.dsp.so/4ezMyw7n-insights-projects-new-scores). Impact Score is the
7+
one exception — package-mediated by design, computed here as MAX(packages.impact) over the repo's
8+
own published packages (via packageRepos), not an average over unrelated packages.
9+
- `repoUrl` is the repository URL — the join key back to `repositories`/`repos`.
10+
- `maintainerHealthScoreV2` (0-40) — bus factor, org diversity, maintainer responsiveness.
11+
- `securitySupplyChainScoreV2` (0-35) — open vulnerabilities, OpenSSF Scorecard, security practices,
12+
dependency health (checked against the repo's own published packages' dependencies, not an average
13+
across unrelated packages), supply chain integrity (hardcoded 0 — provenance/2FA data not yet piped).
14+
- `developmentActivityScoreV2` (0-25) — release cadence, commit activity, issue resolution, PR merge health.
15+
- `healthScoreV2` (0-100) is the sum of the three categories above, clamped to 100.
16+
- `lifecycleLabelV2` — per-repo lifecycle state (active/stable/declining/abandoned/archived), computed
17+
via the spec's decision tree (archived flag > abandoned > declining > stable > active, first match
18+
wins), or NULL when the repo has zero commits and zero issues/PRs in every window checked (2026-07-24,
19+
IN-1196 — no usable activity signal at all). Project-level rollup uses best-state-wins precedence in
20+
project_insights_copy.pipe.
21+
- `impactScore` (0-100) — MAX(packages.impact) * 100 over packages published by this repo. NULL when
22+
the repo has no linked packages.
23+
- No graceful-degradation/signal-coverage redistribution yet — a repo with zero signal in a category
24+
scores 0 there rather than having points redistributed from available categories.
25+
26+
SCHEMA >
27+
`repoUrl` String,
28+
`maintainerHealthScoreV2` Nullable(UInt8),
29+
`securitySupplyChainScoreV2` Nullable(UInt8),
30+
`developmentActivityScoreV2` Nullable(UInt8),
31+
`healthScoreV2` Nullable(UInt8),
32+
`lifecycleLabelV2` Nullable(String),
33+
`impactScore` Nullable(UInt8)
34+
35+
ENGINE MergeTree
36+
ENGINE_SORTING_KEY repoUrl
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DESCRIPTION >
2+
- `health_score_v2_security_ds` holds the per-repo Security & Supply Chain category score (0-35)
3+
for Health Score v2. Populated by `health_score_v2_security.pipe`.
4+
- `repoUrl` is the repository URL — the join key back to `repositories`.
5+
- `securitySupplyChainScoreV2` — NULL when covered sub-signal weight is <40% of the 35pt max
6+
(spec Layer 1 graceful degradation), otherwise the rescaled 0-35 score.
7+
8+
SCHEMA >
9+
`repoUrl` String,
10+
`securitySupplyChainScoreV2` Nullable(UInt8)
11+
12+
ENGINE MergeTree
13+
ENGINE_SORTING_KEY repoUrl

services/libs/tinybird/datasources/project_insights_copy_ds.datasource

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ DESCRIPTION >
3232
- `activeContributorsPrevious365Days` column is the unique count of active contributors in the previous 365 days (365-730 days ago).
3333
- `activeOrganizationsPrevious365Days` column is the unique count of active organizations in the previous 365 days (365-730 days ago).
3434
- `status` column is the current status of the project (e.g., 'active', 'archived').
35+
- `healthScoreV2` column is the Akrites-methodology composite health score (0-100), averaged from `ossPackages_enriched_ds.healthScore` across the project's linked packages. Distinct from the legacy `healthScore` column (community/contributor-based); null when the project has no linked package data.
36+
- `healthLabel` column buckets `healthScoreV2`: 'excellent' (85+), 'healthy' (70-84), 'fair' (50-69), 'concerning' (30-49), 'critical' (<30). Lowercase, matches `ossPackages_enriched_ds.healthLabel` convention.
37+
- `lifecycleLabel` column is the project's aggregated maintenance state across its packages, using best-state-wins precedence (active > stable > declining > abandoned > archived) over `ossPackages_enriched_ds.lifecycleLabel`.
38+
- `impactScore` column is the average Osprey criticality `impact` (0-100) across the project's packages. Null when the project has no linked package data.
39+
- `impactLabel` column buckets `impactScore`: 'foundational' (85-100), 'major' (60-84), 'moderate' (30-59), 'minor' (0-29). Null when `impactScore` is null.
3540

3641
TAGS "Project insights", "Metrics"
3742

@@ -65,7 +70,12 @@ SCHEMA >
6570
`starsPrevious365Days` UInt64,
6671
`forksPrevious365Days` UInt64,
6772
`activeContributorsPrevious365Days` UInt64,
68-
`activeOrganizationsPrevious365Days` UInt64
73+
`activeOrganizationsPrevious365Days` UInt64,
74+
`healthScoreV2` Nullable(UInt8),
75+
`healthLabel` Nullable(String),
76+
`lifecycleLabel` Nullable(String),
77+
`impactScore` Nullable(UInt8),
78+
`impactLabel` Nullable(String)
6979

7080
ENGINE MergeTree
7181
ENGINE_SORTING_KEY type, id

services/libs/tinybird/pipes/collection_contributor_dependency.pipe

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,66 @@ DESCRIPTION >
88
replacement for that table is `collection_contributors_leaderboard` (a differently-shaped
99
pipe - see its own DESCRIPTION for why a naive collectionSlug branch on the single-project
1010
leaderboard pipe isn't used there either).
11-
- Combines data from `collection_contributors_leaderboard` and `active_contributors` (both
12-
already collectionSlug-aware) to provide dependency risk assessment at collection scale.
11+
- **Two paths, branched on whether any optional filter is present:**
12+
- **Precomputed path (no `repos`/`startDate`/`endDate`/`platform`/`activity_type` and
13+
`includeCollaborations` absent or `false`)** - this is what the real frontend widget always
14+
requests on first paint, before a user touches a filter control. Reads
15+
`collection_contributor_dependency_copy_ds`, a daily `TYPE COPY` materialization (see
16+
`collection_contributor_dependency_copy.pipe`), instead of recomputing live. This exists
17+
because the live computation - via `collection_contributors_leaderboard`'s
18+
`collection_member_aggregates` node - must fully materialize a `GROUP BY af.memberId` over the
19+
entire filtered activity set before it can ORDER BY/LIMIT (LIMIT only reduces output size, not
20+
scan/aggregation cost), which at collection scale (e.g. `cncf` has 149,677 distinct
21+
contributors) triggered a Tinybird production rate-limit/concurrency alert on `lfx_insights`
22+
from timeouts under load.
23+
- **Live path (any of those filters present)** - unavoidable, since filters are genuine dynamic
24+
user input and the precomputed table only covers the unfiltered default. Unchanged from the
25+
prior implementation: combines `collection_contributors_leaderboard` and `active_contributors`
26+
(both already collectionSlug-aware).
27+
- Precomputed path only supports `limit=100` (the only value the frontend ever sends for this
28+
widget, and the only value the copy pipe materializes) - if `limit` is explicitly set to
29+
anything else, falls through to the live path even with no other filters present.
1330
- Parameters:
1431
- `collectionSlug`: Required collection slug (e.g., 'cncf') - inherited from `segments_filtered_by_collection`.
15-
- `repos`: Optional array of repository URLs for filtering (inherited from `segments_filtered_by_collection`)
16-
- `startDate`: Optional DateTime filter for activities after timestamp (e.g., '2024-01-01 00:00:00')
17-
- `endDate`: Optional DateTime filter for activities before timestamp (e.g., '2024-12-31 23:59:59')
18-
- `platform`: Optional string filter for source platform (e.g., 'github', 'discord', 'slack')
19-
- `activity_type`: Optional string filter for single activity type (e.g., 'authored-commit')
20-
- `includeCollaborations`: Optional boolean, defaults to false - same toggle as the single-project pipe.
32+
- `repos`: Optional array of repository URLs for filtering (inherited from `segments_filtered_by_collection`). Presence routes to the live path.
33+
- `startDate`: Optional DateTime filter for activities after timestamp (e.g., '2024-01-01 00:00:00'). Presence routes to the live path.
34+
- `endDate`: Optional DateTime filter for activities before timestamp (e.g., '2024-12-31 23:59:59'). Presence routes to the live path.
35+
- `platform`: Optional string filter for source platform (e.g., 'github', 'discord', 'slack'). Presence routes to the live path.
36+
- `activity_type`: Optional string filter for single activity type (e.g., 'authored-commit'). Presence routes to the live path.
37+
- `includeCollaborations`: Optional boolean, defaults to false - same toggle as the single-project pipe. `true` routes to the live path.
2138
- `limit`: Optional integer for the number of top contributors to rank before finding the 51%
2239
cutoff. Inherited by `collection_contributors_leaderboard` (this pipe has no default of its
2340
own), whose own default is 10 if omitted - callers doing bus-factor analysis should pass
2441
limit=100 explicitly (same convention as the single-project `contributor_dependency.pipe` /
25-
`contributors_leaderboard.pipe` pair) to reliably reach the 51% cutoff.
42+
`contributors_leaderboard.pipe` pair) to reliably reach the 51% cutoff. A value other than 100
43+
routes to the live path.
2644
- Response: `id`, `displayName`, `githubHandleArray`, `contributionCount`, `contributionPercentage`, `roles`, `contributionPercentageRunningTotal`, `totalContributorCount`
2745

2846
TAGS "Insights, Widget", "Collection", "Contributors"
2947

48+
NODE collection_contributor_dependency_precomputed
49+
DESCRIPTION >
50+
Precomputed path: point lookup into the daily materialization, filtered to the rows the live
51+
pipe's WHERE clause would itself return (`isBusFactorCutoff = 1`), ordered by rank.
52+
53+
SQL >
54+
%
55+
SELECT
56+
id,
57+
displayName,
58+
githubHandleArray,
59+
contributionCount,
60+
contributionPercentage,
61+
roles,
62+
contributionPercentageRunningTotal,
63+
totalContributorCount
64+
FROM collection_contributor_dependency_copy_ds
65+
WHERE
66+
collectionSlug
67+
= {{ String(collectionSlug, description="Filter by collection slug", required=True) }}
68+
AND isBusFactorCutoff = 1
69+
ORDER BY rank ASC
70+
3071
NODE collection_contributions_percentage_running_total
3172
SQL >
3273
%
@@ -50,3 +91,19 @@ SQL >
5091
contributionPercentageRunningTotal <= 51
5192
OR (contributionPercentageRunningTotal - contributionPercentage < 51)
5293
ORDER BY contributionPercentageRunningTotal ASC
94+
95+
NODE collection_contributor_dependency_result
96+
DESCRIPTION >
97+
Routes between the precomputed and live paths. Precomputed path applies only when no optional
98+
filter is present and `limit` (if given) is exactly 100 - the exact shape the frontend always
99+
requests on first paint.
100+
101+
SQL >
102+
%
103+
{% if not defined(repos) and not defined(startDate) and not defined(endDate) and not defined(
104+
platform
105+
) and not defined(activity_type) and not Boolean(includeCollaborations, false) and Int32(
106+
limit, 100
107+
) == 100 %} SELECT * FROM collection_contributor_dependency_precomputed
108+
{% else %} SELECT * FROM collection_contributions_percentage_running_total
109+
{% end %}

0 commit comments

Comments
 (0)