Skip to content

feat: health Score v2 (Akrites) scoring pipeline, lifecycle, and impact score (CM-IN-1196) - #4394

Merged
gaspergrom merged 11 commits into
mainfrom
feat/IN-1196-akrites-project-scores
Jul 28, 2026
Merged

feat: health Score v2 (Akrites) scoring pipeline, lifecycle, and impact score (CM-IN-1196)#4394
gaspergrom merged 11 commits into
mainfrom
feat/IN-1196-akrites-project-scores

Conversation

@gaspergrom

Copy link
Copy Markdown
Contributor

Summary

Adds the Health Score v2 (Akrites methodology) scoring pipeline: per-repo maintainer health, security/supply-chain, and development-activity category scores, plus a lifecycle classifier and an impact score, rolled up to project level. Also fixes several correctness bugs found during data verification against production before rollout.

  • Split Health Score v2 computation into 5 independently-materialized category pipes (maintainer/security/development/lifecycle/impact) to keep copy-job runtime down — a single combined pipe was re-scanning base tables per category and had grown from ~13min to 20-28+ min per run.
  • healthScoreV2 applies spec-defined graceful degradation: if 1-2 of 3 categories are unavailable, the score rescales across only the available categories rather than defaulting to a fabricated 0; NULL only when all three are unavailable.
  • Bus factor fix: the maintainer-health bus-factor score counted maintainer role records purely by an unexpired endDate (a manually-curated field that stays at a "still active" sentinel indefinitely unless someone explicitly closes the role). This let archived/dead repos keep a maxed-out bus-factor score from maintainers with no real activity in years, which combined with the graceful-degradation rescale to produce healthScoreV2 = 100 on dead repos. Bus factor now also requires the maintainer to have actual activityRelations activity on that repo within the same trailing 12-month window. Verified against production: Xamarin.Forms and SpaceVim (archived) drop from a maxed bus-factor score to 0; kubernetes/kubernetes stays healthy and unaffected.
  • Lifecycle fallback fix: the lifecycle decision tree's final ELSE branch always resolved to 'active' whenever a repo failed every other branch, including repos with zero commits, zero issues, and zero PRs in every window checked (confirmed live on a fully dormant, non-archived repo with no commit-timeline data and no signal at all). The chain now returns NULL for that case instead of a fabricated 'active', propagated correctly through:
    • health_score_v2_lifecycle_ds — widened lifecycleLabelV2 from String to Nullable(String) (was going to hard-fail the copy job on any NULL insert).
    • health_score_v2.pipe — removed a coalesce(lifecycleLabelV2, 'active') that would have silently erased the NULL back into 'active' one hop downstream.
    • project_insights_copy.pipe — the project-level "best-state-wins" rollup already drops NULL entries for free via groupArray, but an all-NULL or fully-excluded project produces an empty array, and arrayElement([], 1) on an empty array silently returns '' rather than NULL. Added an explicit guard. While tracing this, found the actual root cause of 5 pre-existing blank-lifecycleLabel projects (each project's sole repo has repositories.excluded = true) — a separate, pre-existing data situation, not fixed here, but no longer surfaces as a silent empty string.

All fixes verified against live production Tinybird data before merge, including full-chain propagation checks and regression checks against known-healthy projects (kubernetes, projects with real but thin activity signal).

Test plan

  • tb check passes on all changed pipes/datasources
  • Verified bus-factor fix against production data: archived repos (Xamarin.Forms, SpaceVim, HPCToolkit, Ursa) drop from healthScoreV2=100 to realistic scores (22-62); active repos (kubernetes, GravityView, Hitomi-Downloader, binutils-gdb) unaffected
  • Verified lifecycle NULL fix against production data: a genuinely zero-signal repo (blocknetdx/blocknet) resolves to NULL at both repo and project level; archived repos still correctly resolve to 'archived' even with zero activity; repos with any real signal still resolve to a real state, not NULL
  • Verified datasource schema widening (StringNullable(String)) is a non-breaking, metadata-only ClickHouse ALTER
  • Deploy to staging, confirm pipes deploy cleanly (staging data is known to be sparse, so this validates deployability not data correctness)
  • Deploy to production, re-validate full chain against live data post-deploy

Deploy notes

Deploy order matters — datasources before pipes, then pipes in dependency order:

  1. health_score_v2_lifecycle_ds.datasource, health_score_v2_repo_copy_ds.datasource (schema changes)
  2. health_score_v2_lifecycle.pipe, health_score_v2_maintainer.pipe (independent, both feed health_score_v2)
  3. health_score_v2.pipe (reads both category datasources above)
  4. project_insights_copy.pipe (reads health_score_v2_repo_copy_ds)

After pushing pipes, the copy jobs need a forced run (tbc <pipe> --yes) rather than waiting for the nightly cron, in the same order, so the NULL/bus-factor fixes propagate through the full chain before this is user-visible.

…(IN-1196)

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
…althScore (IN-1196)

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
- Split health_score_v2 into 5 category pipes plus a lightweight combiner,
  fixing a perf regression from re-scanning base tables per category
- Fix graceful degradation: availability flags checked IS NOT NULL on
  non-Nullable columns, which ClickHouse fills with 0/'' not NULL on
  LEFT JOIN miss, so degradation never triggered
- Fix Impact Score defaulting to 100 instead of NULL for repos with
  no packages (least(NULL, 100) = 100 in ClickHouse)
- Fix Maintainer Responsiveness scoring per product review: GitHub/GitLab
  score 0 (not blocked) when no PR/issue data; Gerrit scores 0 (not
  blocked) when no changeset data, issues excluded since Gerrit has no
  issue tracker; excluded=true repos stay blocked, never scored 0
- project_insights_copy now reads split datasources via cheap LEFT JOIN

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
maintainers_roles_copy_ds.endDate stays at the 1970-01-01 sentinel
indefinitely unless someone explicitly closes the role, so archived/
dead repos retained a maxed-out busFactorScore=18 from maintainers
with no commits or activity in years. Confirmed on Xamarin.Forms,
HPCToolkit, Ursa, and SpaceVim, all archived, all showing
healthScoreV2=100 after Layer 2 rescaling.

Bus factor now also requires the maintainer to have activityRelations
activity on that repo within the same trailing 12-month window
already used for org diversity. Verified against live Tinybird data:
Xamarin.Forms 7->0, SpaceVim 8->0, kubernetes/kubernetes 160->123
(unaffected), torvalds/linux 0->0 (unaffected).

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
…(IN-1196)

Per Joana's review: the lifecycle decision tree's final ELSE always
resolved to 'active' whenever a repo failed every other branch,
including repos with zero commits, zero issues, and zero PRs in every
window checked. Confirmed on blocknetdx/blocknet: no commit-timeline
data and exactly 0 activity everywhere, yet labeled 'active' purely
because no other branch matched.

health_score_v2_lifecycle.pipe now adds an explicit 'unavailable'
branch before the active fallback, triggered only when commits are 0
in both 6-month windows AND issues+PRs are 0 in the 18-month window --
i.e. every signal this pipe looks at is exactly zero, not just one
missing field. Repos with real activity that simply don't clear the
abandoned/declining/stable thresholds (GravityView, Hitomi-Downloader,
bminor/binutils-gdb -- all have substantial issue/PR activity despite
missing repos.lastCommitAt or near-zero recent commits) correctly
still resolve to 'active', unchanged.

project_insights_copy.pipe's project-level best-state-wins rollup used
a hardcoded 5-value priority array that didn't include 'unavailable' --
ClickHouse's indexOf() returns 0 for values not in the array, which
would have sorted 'unavailable' as better than 'active' (index 1),
letting one data-starved repo incorrectly override a genuinely active
project. Added 'unavailable' explicitly as the lowest-priority state.

Verified against live Tinybird data: blocknetdx/blocknet unavailable
(was active); GravityView/Hitomi-Downloader/bminor-binutils-gdb still
active (unchanged, real signal present); kubernetes/kubernetes
unaffected (6331 commits last 6mo).

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Per Joana's follow-up: prefer NULL at the data layer for insufficient
signal, consistent with how healthScoreV2 already represents it, and
let the UI derive the Unavailable chip from NULL rather than carrying
a literal string through the pipeline.

health_score_v2_lifecycle.pipe: the zero-signal branch (zero commits
in both 6-month windows AND zero issues/PRs in the 18-month window)
now returns NULL instead of 'unavailable'. Archived repos are
explicitly excluded from this check so they still correctly resolve
to 'archived' even when they also have zero activity. Re-verified:
blocknetdx/blocknet -> NULL (was 'unavailable', originally 'active');
xamarin/Xamarin.Forms -> 'archived' (unaffected); GravityView,
Hitomi-Downloader, bminor/binutils-gdb -> 'active' (unaffected, real
signal present).

project_insights_copy.pipe: the project-level best-state-wins rollup
no longer needs 'unavailable' in its priority array (back to the
original 5 values), since groupArray() already drops NULL entries for
free when at least one repo in the project has a real state. But an
all-NULL or fully-excluded project makes groupArray() return an EMPTY
array, and arrayElement([], 1) on an empty array silently returns an
empty string, not NULL. Added an explicit empty-array guard so that
case resolves to a real NULL too.

While tracing this, found the actual root cause of 5 pre-existing
blank-lifecycleLabel projects (Charliecloud, GNU Binutils and GDB,
GravityView, GridGain Community Edition, Hitomi-Downloader): each
project's sole repo has repositories.excluded = true, so the pipe's
own WHERE rep.excluded = false filter drops every repo, groupArray()
sees zero rows, and the old unguarded arrayElement returned ''. This
empty-array guard fixes the class of bug generally; the excluded=true
repo assignment for those 5 projects is a separate, pre-existing data
situation, unrelated to IN-1196, not addressed here.

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Two chain breaks found during pre-deploy validation of 7a33bf7 that
would have made the NULL-lifecycle fix fail outright or silently
revert:

health_score_v2_lifecycle_ds.datasource declared lifecycleLabelV2 as a
plain String, not Nullable(String). Inserting NULL into a non-nullable
ClickHouse column throws CANNOT_INSERT_NULL_IN_ORDINARY_COLUMN, so the
lifecycle pipe's nightly copy job would hard-fail on its very first
run after deploy, for any repo hitting the new NULL branch (confirmed:
blocknetdx/blocknet is one right now). Widened to Nullable(String).

health_score_v2.pipe wrapped the pass-through in
coalesce(l.lifecycleLabelV2, 'active'), which would have silently
converted the upstream NULL back into a fabricated 'active' one hop
downstream, erasing the fix's effect before it ever reached
health_score_v2_repo_copy_ds (whose own schema already correctly
declares lifecycleLabelV2 as Nullable(String) — no change needed
there). Confirmed live that this LEFT JOIN has zero actual row-misses
across all repos, so the coalesce was never serving a real missing

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Copilot AI review requested due to automatic review settings July 24, 2026 13:39
@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large new analytics pipeline and changed scoring semantics affect project/repo insights visibility; collection dependency routing must stay equivalent between precomputed and live paths under load.

Overview
Introduces the Akrites Health Score v2 Tinybird stack: five nightly COPY category pipes (maintainer, security, development, lifecycle, impact) materialize per-repo scores into small datasources; health_score_v2.pipe joins them into health_score_v2_repo_copy_ds with spec Layer 2 rescaling when categories are missing. Correctness fixes include bus factor counting only maintainers with 12-month activityRelations on the repo (not stale role rows), lifecycle returning NULL for repos with no activity signal (and Nullable(String) + removing coalesce(..., 'active') downstream), and project rollup guarding empty groupArray so lifecycle is not a silent ''.

project_insights_copy_ds and the project_insights / project_repo_insights serving pipes gain healthScoreV2, healthLabel, lifecycleLabel, impactScore, and impactLabel (repo-level join + project-level rollup from health_score_v2_repo_copy_ds).

Separately, collection_contributor_dependency adds a daily precomputed path via collection_contributor_dependency_copy.pipecollection_contributor_dependency_copy_ds for the default unfiltered limit=100 widget load; filtered requests still use the live leaderboard path. Minor SQL formatting in organizations_filtered and pull_request_analysis_baseline_merge_MV only.

Reviewed by Cursor Bugbot for commit 65a1273. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conventional Commits FTW!

SELECT channel AS repoUrl, count(DISTINCT organizationId) AS orgCount
FROM activityRelations
WHERE timestamp > now() - INTERVAL 12 MONTH AND organizationId != ''
GROUP BY channel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bus factor counts non-contribution activity

Medium Severity

The bus-factor fix requires recent activityRelations rows on the repo, but the subquery does not restrict activity type. Stars, forks, and other non-contribution events can satisfy the join, so maintainers with no real maintenance work in the window may still inflate bus factor—similar to the stale role-record issue this change was meant to fix.

Fix in Cursor Fix in Web

Triggered by learned rule: Tinybird pipes counting contributors must filter by activityTypes

Reviewed by Cursor Bugbot for commit a20d1bc. Configure here.

pm.activeContributorsPrevious365Days AS activeContributorsPrevious365Days,
pm.activeOrganizationsPrevious365Days AS activeOrganizationsPrevious365Days
pm.activeOrganizationsPrevious365Days AS activeOrganizationsPrevious365Days,
toUInt8(round(hv2.healthScoreV2)) AS healthScoreV2,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL health score becomes zero

High Severity

Project and repo rows set healthScoreV2 with toUInt8(round(hv2.healthScoreV2)) without a NULL guard, unlike upstream pipes that use toNullable. Missing or unavailable scores can become 0, which is a valid “critical” score and contradicts healthLabel when that field is NULL. When every included repo has NULL healthScoreV2, avg can yield NaN; NaN is not treated as NULL in the label multiIf, so the project can show healthLabel critical while the score is wrong.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a20d1bc. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds the Akrites Health Score v2 pipeline and exposes repository/project health, lifecycle, and impact metrics.

Changes:

  • Materializes five independent scoring categories and combines available health signals.
  • Adds lifecycle and impact scoring with project-level rollups.
  • Exposes the new metrics through insights endpoints and datasources.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
pipes/project_repo_insights.pipe Exposes Akrites fields for projects and repositories.
pipes/project_insights.pipe Exposes Akrites project fields.
pipes/project_insights_copy.pipe Rolls up and labels repository scores.
pipes/health_score_v2.pipe Combines category scores with graceful degradation.
pipes/health_score_v2_security.pipe Computes security and supply-chain health.
pipes/health_score_v2_maintainer.pipe Computes maintainer health.
pipes/health_score_v2_lifecycle.pipe Classifies repository lifecycle.
pipes/health_score_v2_impact.pipe Computes package-mediated impact.
pipes/health_score_v2_development.pipe Computes development activity health.
datasources/project_insights_copy_ds.datasource Adds Akrites insight columns.
datasources/health_score_v2_security_ds.datasource Stores security scores.
datasources/health_score_v2_repo_copy_ds.datasource Stores combined repository results.
datasources/health_score_v2_maintainer_ds.datasource Stores maintainer scores.
datasources/health_score_v2_lifecycle_ds.datasource Stores lifecycle classifications.
datasources/health_score_v2_impact_ds.datasource Stores raw impact scores.
datasources/health_score_v2_development_ds.datasource Stores development scores.
Comments suppressed due to low confidence (3)

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:85

  • All three values only use issues opened in the last 18 months, but the aggregation currently reads every partition in issues_analyzed. Add the 18-month predicate to WHERE so ClickHouse can prune old yearly partitions.
        FROM issues_analyzed
        GROUP BY channel

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:90

  • The metric only counts PRs from the last 18 months, yet this scans all yearly partitions in pull_requests_analyzed. Push the same time condition into WHERE to prune historical partitions.
        FROM pull_requests_analyzed
        GROUP BY channel

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:24

  • This statement contradicts the implemented Layer 2 graceful degradation in health_score_v2.pipe: unavailable categories are excluded and remaining category scores are rescaled, while the result is NULL only when all categories are unavailable. The earlier “sum” description is also incomplete for that case.
    - No graceful-degradation/signal-coverage redistribution yet — a repo with zero signal in a category
      scores 0 there rather than having points redistributed from available categories.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

+ if(d.developmentActivityScoreV2 IS NOT NULL, 25, 0)) AS coveredCategoryWeight,
l.lifecycleLabelV2 AS lifecycleLabelV2,
i.impactScoreRaw AS impactScoreRaw
FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS base
(rd.url != '') AS securityPracticesAvailable,
multiIf(coalesce(dh.vulnerableDeps,0) = 0, 5, dh.vulnerableDeps <= 2, 3, dh.vulnerableDeps <= 5, 1, 0) AS dependencyHealthScore,
(pkgs.repoUrl != '') AS dependencyHealthAvailable
FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS allRepos
+ multiIf(prs.medianMergeS < 604800, 2, prs.medianMergeS < 2592000, 1, 0)) AS prMergeScore
FROM (
SELECT base.url AS repoUrl, rc.lastCommitAt AS lastCommitAt
FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base
SELECT
base.url AS repoUrl,
max(toFloat64OrNull(pk.impact)) * 100 AS impactScoreRaw
FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base
SELECT DISTINCT url AS repoUrl,
(domain(url) = 'review.opendev.org' OR domain(url) LIKE 'gerrit.%') AS isGerrit,
excluded AS isExcluded
FROM repositories WHERE deletedAt IS NULL
countIf(timestamp > now() - INTERVAL 6 MONTH) AS commitsLast6m,
countIf(timestamp <= now() - INTERVAL 6 MONTH AND timestamp > now() - INTERVAL 12 MONTH) AS commitsPrior6m
FROM activityRelations_deduplicated_cleaned_bucket_union
WHERE type = 'authored-commit'
Comment on lines +35 to +38
- `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.
- `healthLabel` column buckets `healthScoreV2`: 'excellent' (85+), 'healthy' (70-84), 'fair' (50-69), 'concerning' (30-49), 'critical' (<30). Lowercase, matches `ossPackages_enriched_ds.healthLabel` convention.
- `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`.
- `impactScore` column is the average Osprey criticality `impact` (0-100) across the project's packages. Null when the project has no linked package data.
- `pageSize`: Optional integer for result limit, defaults to 10
- `page`: Optional integer for pagination offset calculation, defaults to 0
- Response: Project records with all insights metrics including achievements as array of (leaderboardType, rank, totalCount) tuples
- `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the project has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.
- `pageSize`: Optional integer for result limit, defaults to 10
- `page`: Optional integer for pagination offset calculation, defaults to 0
- Response: Project and repository records with insights metrics
- `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the record has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.
Comment on lines +11 to +13
- `securitySupplyChainScoreV2` (0-35) — open vulnerabilities, OpenSSF Scorecard, security practices,
dependency health (checked against the repo's own published packages' dependencies, not an average
across unrelated packages), supply chain integrity (hardcoded 0 — provenance/2FA data not yet piped).
…uests

CNCF (149,677 contributors) triggered a Tinybird production rate-limit
alert (concurrency:1) on collection_contributor_dependency due to
timeouts under load. The live computation must fully materialize a
GROUP BY af.memberId over the entire collection's activity set before
it can ORDER BY/LIMIT, which doesn't scale for large collections.

Adds a daily copy pipe that precomputes the default (unfiltered)
bus-factor result per collection. The serving pipe now reads the
precomputed table when no optional filter is present (the frontend's
first-paint shape) and falls through to the live path when a filter
is set. Verified against production: CNCF requests dropped from
6-14s / ~150M rows scanned to ~20-130ms / 8,192 rows scanned.

Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Copilot AI review requested due to automatic review settings July 27, 2026 07:25

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 5 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ 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 a3e0359. Configure here.

'archived',
(r.lastCommitAt < now() - INTERVAL 18 MONTH)
AND coalesce(w.issuesInWindow18m, 0) + coalesce(p.prsInWindow18m, 0) = 0,
'abandoned',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL lifecycle shadows abandoned

High Severity

The new no-signal NULL branch requires zero commits in the 12-month activityRelations windows and zero issues/PRs, but does not consider lastCommitAt. Repos with an old lastCommitAt and no issues/PRs — the normal abandoned case — also have zero ingested commits in those windows, so they hit NULL before the abandoned branch can run. In consistent data, abandoned becomes effectively unreachable.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3e0359. Configure here.

SELECT channel AS repoUrl, count(DISTINCT organizationId) AS orgCount
FROM activityRelations
WHERE timestamp > now() - INTERVAL 12 MONTH AND organizationId != ''
GROUP BY channel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw activityRelations in maintainer score

Medium Severity

Bus-factor and org-diversity joins read raw activityRelations, while the sibling Health Score v2 pipes and the datasource docs require activityRelations_deduplicated_cleaned_bucket_union for analytics. Uncleaned rows (duplicates, rows meant to be filtered out) can inflate recent-activity maintainer and org counts and skew Maintainer Health.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3e0359. Configure here.

INNER JOIN members_sorted m ON m.id = r.id
LEFT JOIN
(SELECT memberId, groupUniqArray(role) AS roles FROM maintainers_roles_copy_ds GROUP BY memberId) mr
ON mr.memberId = r.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roles not scoped to collection

Medium Severity

The precomputed copy aggregates maintainers_roles_copy_ds roles by memberId only, with no collection/insightsProjectId filter. The live path uses member_roles, which restricts roles to projects in the collection. Precomputed responses can show maintainer roles from outside the collection, so default (unfiltered) widget loads disagree with the filtered live path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a3e0359. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (8)

services/libs/tinybird/pipes/health_score_v2_maintainer.pipe:81

  • repositories is a versioned ReplacingMergeTree, but this query filters deletedAt and reads mutable excluded before collapsing versions. A repository whose exclusion flag changed can therefore produce two rows with the same URL and duplicate the category output. Read FINAL, as established in project_insights_copy.pipe:131 and repositories_populated_copy.pipe:7.
                FROM repositories WHERE deletedAt IS NULL

services/libs/tinybird/pipes/health_score_v2.pipe:52

  • Because repositories is a versioned ReplacingMergeTree, filtering before FINAL leaves old non-deleted versions visible and can retain soft-deleted repositories in the final score datasource. Use the current version before applying deletedAt, consistent with repositories_populated_copy.pipe:7.
        FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:60

  • This reads mutable archived values from every physical version of the repositories ReplacingMergeTree. If a repository was archived or unarchived, DISTINCT url, archived emits both states and materializes conflicting lifecycle rows for one URL. Collapse with FINAL before filtering/selecting mutable fields.
        FROM (SELECT DISTINCT url, archived FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/project_insights_copy.pipe:176

  • The displayed project score is rounded to an integer, but the label thresholds use the unrounded average. For example, 84.6 is exposed as healthScoreV2 = 85 while receiving healthLabel = 'healthy', contradicting the documented 85+ “excellent” band. Apply the thresholds to the same rounded value.
        toUInt8(round(hv2.healthScoreV2)) AS healthScoreV2,
        multiIf(
            hv2.healthScoreV2 IS NULL,
            NULL,
            hv2.healthScoreV2 >= 85,

services/libs/tinybird/datasources/project_insights_copy_ds.datasource:39

  • These datasource descriptions document the previous package-derived calculation, but the new pipe computes health from per-repository category scores, lifecycle across repository labels, and impact as a maximum rather than an average. This is the contract consumers will consult, so update it to match the implemented aggregation and null conditions.
    - `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.
    - `healthLabel` column buckets `healthScoreV2`: 'excellent' (85+), 'healthy' (70-84), 'fair' (50-69), 'concerning' (30-49), 'critical' (<30). Lowercase, matches `ossPackages_enriched_ds.healthLabel` convention.
    - `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`.
    - `impactScore` column is the average Osprey criticality `impact` (0-100) across the project's packages. Null when the project has no linked package data.
    - `impactLabel` column buckets `impactScore`: 'foundational' (85-100), 'major' (60-84), 'moderate' (30-59), 'minor' (0-29). Null when `impactScore` is null.

services/libs/tinybird/pipes/project_insights.pipe:15

  • This public endpoint description ties all five fields to linked package data, but health and lifecycle are repository-derived and become null based on signal coverage; only impact is package-mediated. Update the nullability note so API consumers do not infer the wrong prerequisite.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the project has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/project_repo_insights.pipe:14

  • This endpoint description says the Akrites fields may be null due to missing package data, but repository health and lifecycle do not depend on packages. Only impact has that prerequisite; health/lifecycle nullability reflects unavailable repository signals.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the record has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:24

  • This states that graceful degradation is not implemented, directly contradicting health_score_v2.pipe:33-49, which rescales over available category weights. The earlier “sum” description is consequently also inaccurate whenever a category is unavailable. Document the actual rescaling behavior in one place.
    - No graceful-degradation/signal-coverage redistribution yet — a repo with zero signal in a category
      scores 0 there rather than having points redistributed from available categories.

Comment on lines +84 to +93
SELECT mr.repoUrl AS repoUrl, count(DISTINCT mr.memberId) AS busFactorCount
FROM maintainers_roles_copy_ds mr
INNER JOIN (
SELECT DISTINCT memberId, channel AS repoUrl
FROM activityRelations
WHERE timestamp > now() - INTERVAL 12 MONTH
) AS recentActivity
ON recentActivity.memberId = mr.memberId AND recentActivity.repoUrl = mr.repoUrl
WHERE mr.endDate = '1970-01-01 00:00:00' OR mr.endDate > now() - INTERVAL 12 MONTH
GROUP BY mr.repoUrl
Comment on lines +145 to +147
LEFT JOIN
(SELECT memberId, groupUniqArray(role) AS roles FROM maintainers_roles_copy_ds GROUP BY memberId) mr
ON mr.memberId = r.id
collectionSlug
= {{ String(collectionSlug, description="Filter by collection slug", required=True) }}
AND isBusFactorCutoff = 1
ORDER BY rank ASC
Comment on lines +149 to +151
TYPE COPY
TARGET_DATASOURCE collection_contributor_dependency_copy_ds
COPY_MODE replace

SQL >
%
{% if not defined(repos) and not defined(startDate) and not defined(endDate) and not defined(platform) and not defined(activity_type) and not Boolean(includeCollaborations, false) and Int32(limit, 100) == 100 %}
@gaspergrom gaspergrom changed the title feat: Health Score v2 (Akrites) scoring pipeline, lifecycle, and impact score (CM-IN-1196) feat: health Score v2 (Akrites) scoring pipeline, lifecycle, and impact score (CM-IN-1196) Jul 28, 2026
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
Copilot AI review requested due to automatic review settings July 28, 2026 08:29
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (21)

services/libs/tinybird/pipes/project_insights_copy.pipe:176

  • The stored project score is rounded, but the label thresholds use the unrounded average. For example, an average of 84.6 is emitted as healthScoreV2 = 85 while still labeled healthy, contradicting the documented 85+ excellent band. Apply thresholds to the same rounded value that is stored.
        pm.activeContributorsPrevious365Days AS activeContributorsPrevious365Days,
        pm.activeOrganizationsPrevious365Days AS activeOrganizationsPrevious365Days,
        toUInt8(round(hv2.healthScoreV2)) AS healthScoreV2,
        multiIf(
            hv2.healthScoreV2 IS NULL,

services/libs/tinybird/pipes/collection_contributor_dependency.pipe:103

  • This routing gate changes and bypasses existing request semantics in two cases: an omitted limit previously inherits the leaderboard default of 10 but is treated here as 100, and the supported activity_types filter is ignored. Such requests silently receive the unfiltered top-100 materialization. Only use the copy when limit=100 is explicitly present and no activity_types filter is present.
    {% if not defined(repos) and not defined(startDate) and not defined(endDate) and not defined(

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:147

  • This role aggregation is global, so a contributor's roles from unrelated projects/collections leak into every precomputed collection response. The live path scopes maintainers_roles_copy_ds.insightsProjectId through segments_filtered_by_collection (member_roles.pipe:8-10). Join roles to the current collectionSlug's project IDs before grouping.
            SELECT memberId, groupUniqArray(role) AS roles
            FROM maintainers_roles_copy_ds

services/libs/tinybird/pipes/health_score_v2.pipe:52

  • repositories is a versioned ReplacingMergeTree; filtering deletedAt before collapsing versions lets an older non-deleted version keep a soft-deleted repository in the output. Existing mutable-field queries use repositories FINAL (for example, repos_to_channels_excluded.pipe:15).
                    coalesce(m.maintainerHealthScoreV2, 0)

services/libs/tinybird/pipes/health_score_v2_security.pipe:39

  • This filters deletedAt on the versioned repositories datasource without FINAL, so stale non-deleted versions continue producing security rows for soft-deleted repositories. Collapse versions before applying mutable-field filters, as established in repos_to_channels_excluded.pipe:15.
                ) AS coveredWeight

services/libs/tinybird/pipes/health_score_v2_maintainer.pipe:81

  • Because repositories is a ReplacingMergeTree, this pre-FINAL filter can retain soft-deleted rows and can emit duplicate states when excluded changes. That can both score deleted repositories and distort the availability/rescaling calculation. Query the current versions first.
                        multiIf(
                            allRepos.isGerrit,
                            -- Gerrit: PR/changeset response time only; issues don't exist here, never
                            -- penalize their absence

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:60

  • This reads mutable archived/deletedAt values from a ReplacingMergeTree before version collapse. If archival status changed, both old and current (url, archived) pairs can survive DISTINCT, producing multiple lifecycle rows and potentially conflicting labels for one repository. Use FINAL before filtering/grouping.
    FROM

services/libs/tinybird/pipes/health_score_v2_impact.pipe:16

  • Filtering the versioned repositories datasource without FINAL allows an old non-deleted version to retain soft-deleted repositories in the impact materialization. Use the current collapsed row before evaluating deletedAt.
    INNER JOIN packageRepos pr ON pr.repoId = r.id

services/libs/tinybird/pipes/health_score_v2_development.pipe:53

  • Filtering deletedAt before collapsing this ReplacingMergeTree lets stale non-deleted repository versions enter the development score. Follow the existing repositories FINAL convention for mutable fields.
                            0

services/libs/tinybird/pipes/health_score_v2_security.pipe:56

  • vulnerabilities is a ReplacingMergeTree versioned by scannedAt. Counting it without FINAL includes historical OPEN versions after the current row becomes RESOLVED or changes severity, so resolved findings continue lowering the security score. Collapse to the current scan state before counting.
                            round(least(toFloat64OrZero(rd.scorecardScore), 10) * 0.7)
                        ) AS scorecardScorePts,
                        (rd.url != '') AS scorecardAvailable,
                        (
                            2 * coalesce(rd.securityPolicyEnabled, 0)

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:95

  • This counts historical versions from the versioned vulnerabilities datasource. A vulnerability that is now resolved can still have an older OPEN/CRITICAL row counted, incorrectly preventing the stable lifecycle branch. Read the collapsed current state with FINAL.
                channel AS repoUrl,
                countIf(openedAt > now() - INTERVAL 6 MONTH) AS issuesOpenedLast6m,
                countIf(

services/libs/tinybird/datasources/project_insights_copy_ds.datasource:35

  • This describes the obsolete package-derived implementation. The new query averages the independently computed repo-level Akrites scores across enabled, non-excluded repositories, and NULL indicates no available eligible repo score—not merely missing package links.
    - `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.

services/libs/tinybird/datasources/project_insights_copy_ds.datasource:38

  • Both descriptions conflict with the new rollup: lifecycle uses best-state-wins across repository labels, while impact uses the maximum package-mediated impact across repositories, not an average across packages. Document the actual aggregation so consumers do not infer the wrong methodology.
    - `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`.
    - `impactScore` column is the average Osprey criticality `impact` (0-100) across the project's packages. Null when the project has no linked package data.

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:15

  • The final score is not a simple sum: when one or two categories are NULL, this pipe rescales across the available category weights. Update this definition to match the implemented graceful-degradation formula.
    - `healthScoreV2` (0-100) is the sum of the three categories above, clamped to 100.

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:24

  • This statement directly contradicts the new pipeline, which does redistribute category weight and preserves NULL for unavailable categories. Leaving it here makes the datasource contract internally inconsistent.
    - No graceful-degradation/signal-coverage redistribution yet — a repo with zero signal in a category
    scores 0 there rather than having points redistributed from available categories.

services/libs/tinybird/pipes/project_insights.pipe:15

  • Only impactScore depends on linked package data. Health can be NULL due to category coverage, and lifecycle can be NULL due to absent activity signal, even when packages exist. The endpoint contract should describe these independent availability conditions.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the project has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/project_repo_insights.pipe:14

  • This attributes all NULLs to package linkage, but only impact is package-dependent. Health and lifecycle have their own signal-coverage conditions, so this response documentation is misleading for both project and repository records.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the record has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:4

  • This collection bus-factor materialization is unrelated to the Akrites scoring pipeline and is absent from the PR summary, test plan, and deployment order. The repository's commit workflow explicitly says not to bundle unrelated changes (.claude/rules/commit-workflow.md:58-61). Split this performance change into a focused PR so it can be validated and rolled back independently.
DESCRIPTION >
    - `collection_contributor_dependency_copy.pipe` precomputes the DEFAULT (unfiltered) bus-factor
    result for every collection on a daily schedule, writing into `collection_contributor_dependency_copy_ds`.
    - Exists because `collection_contributor_dependency.pipe`'s live computation - via

services/libs/tinybird/pipes/health_score_v2_security.pipe:67

  • The dependency-health subquery reads versioned advisoryPackages and advisories without collapsing them. In particular, an advisory whose current severity was downgraded can still match through its historical HIGH/CRITICAL row, permanently marking dependencies vulnerable. Existing package advisory joins use FINAL on both tables (ossPackages_enriched.pipe:93-94).
                            5,
                            dh.vulnerableDeps <= 2,
                            3,

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:29

  • The stated large-collection behavior is the opposite of the implemented cutoff. If the top 100 reach only 20%, every cumulative total is <= 51, so every row is flagged 1 and returned; the real issue is that the 51% threshold remains unreached beyond the truncated top 100. Correct this operational note.
    interaction, out of scope for this performance fix. `isBusFactorCutoff` will legitimately be 0
    for every row of such a collection; the serving pipe must not assume at least one row per
    collection has `isBusFactorCutoff = 1`.

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:113

  • rank uses memberId DESC, while the running total (and the live response) uses id/memberId ASC for ties. The serving pipe orders by this rank, so tied contributors are returned in reverse order and their cumulative percentages are no longer monotonic. Use the same ascending tie-break for rank and running total; the earlier top-100 selection can retain its boundary behavior.
        ) AS rank,
        sum(ROUND(ma.contributionCount * 100.0 / t.totalContributions, 2)) OVER (
            PARTITION BY ma.collectionSlug
            ORDER BY ROUND(ma.contributionCount * 100.0 / t.totalContributions, 2) DESC, ma.memberId
        ) AS contributionPercentageRunningTotal,

Comment on lines +40 to +41
(busFactorScore + orgDiversityScore + responsivenessScore) AS rawScore,
(busFactorAvailable * 18 + orgDiversityAvailable * 7 + responsivenessAvailable * 15) AS coveredWeight
Copilot AI review requested due to automatic review settings July 28, 2026 08:36
@gaspergrom
gaspergrom merged commit 1ec9f78 into main Jul 28, 2026
14 checks passed
@gaspergrom
gaspergrom deleted the feat/IN-1196-akrites-project-scores branch July 28, 2026 08:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (15)

services/libs/tinybird/pipes/health_score_v2.pipe:63

  • repositories is a versioned ReplacingMergeTree, so filtering deletedAt without FINAL can retain an older non-deleted version after a repository is soft-deleted. That keeps deleted repositories in the final score output. Read the latest version before applying the mutable-field filter, as established in project_insights_copy.pipe:133-135.
            FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_security.pipe:73

  • This mutable deletedAt filter runs before ReplacingMergeTree deduplication, so a stale non-deleted repository version can still produce a security score. Use FINAL here to select the current repository record.
                    FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS allRepos

services/libs/tinybird/pipes/health_score_v2_impact.pipe:14

  • Filtering the versioned repositories datasource without FINAL allows a stale non-deleted version to keep a deleted repository in this impact materialization. Select the current version first.
    FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_development.pipe:105

  • Because repositories is a versioned ReplacingMergeTree, this predicate can match an older non-deleted version and continue calculating development scores for deleted repositories. Apply FINAL before the filter.
                            FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_maintainer.pipe:149

  • The query reads mutable deletedAt and excluded values without FINAL. If both old and current versions exist, DISTINCT can emit two rows for one URL with different isExcluded values, duplicating the category output as well as retaining deleted repositories. Read the latest repository versions first.
                                excluded AS isExcluded
                            FROM repositories
                            WHERE deletedAt IS NULL

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:63

  • archived and deletedAt are mutable fields on a versioned ReplacingMergeTree. Without FINAL, one URL can yield both its old and current archived states, producing conflicting lifecycle rows, while deleted repositories can remain present. Deduplicate to the current version before filtering/grouping.
            FROM (SELECT DISTINCT url, archived FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:150

  • This aggregates roles globally by member, whereas the live path's member_roles pipe first filters roles to the requested collection's project IDs (member_roles.pipe:8-10). A contributor who maintains an unrelated project will therefore expose that unrelated role in this collection's precomputed response. Aggregate and join roles by both collection and member.
            SELECT memberId, groupUniqArray(role) AS roles
            FROM maintainers_roles_copy_ds
            GROUP BY memberId
        ) mr
        ON mr.memberId = r.id

services/libs/tinybird/pipes/collection_contributor_dependency.pipe:107

  • An omitted limit previously inherited the live leaderboard's default of 10, but this default of 100 now routes that request to the top-100 precomputed result. That silently changes response size and cutoff semantics for existing callers that omit limit, contrary to the description's “if given” behavior. Use a non-100 routing default so only an explicit limit=100 takes this path.
    ) and not defined(activity_type) and not Boolean(includeCollaborations, false) and Int32(
        limit, 100
    ) == 100 %} SELECT * FROM collection_contributor_dependency_precomputed

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:3

  • This collection bus-factor materialization and endpoint-routing change is unrelated to the Health Score v2 scope in the title and PR description, and it has its own production behavior and deployment schedule. Bundling it makes validation and rollback of CM-IN-1196 materially riskier; split these collection dependency files into a separately described PR/ticket.
DESCRIPTION >
    - `collection_contributor_dependency_copy.pipe` precomputes the DEFAULT (unfiltered) bus-factor
    result for every collection on a daily schedule, writing into `collection_contributor_dependency_copy_ds`.

services/libs/tinybird/datasources/project_insights_copy_ds.datasource:39

  • These datasource contract descriptions still document the superseded package-level implementation. The new pipe averages repo-level health scores, rolls up repo lifecycle states, and takes the maximum repo/package impact; inaccurate source and nullability semantics will mislead endpoint consumers.
    - `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.
    - `healthLabel` column buckets `healthScoreV2`: 'excellent' (85+), 'healthy' (70-84), 'fair' (50-69), 'concerning' (30-49), 'critical' (<30). Lowercase, matches `ossPackages_enriched_ds.healthLabel` convention.
    - `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`.
    - `impactScore` column is the average Osprey criticality `impact` (0-100) across the project's packages. Null when the project has no linked package data.
    - `impactLabel` column buckets `impactScore`: 'foundational' (85-100), 'major' (60-84), 'moderate' (30-59), 'minor' (0-29). Null when `impactScore` is null.

services/libs/tinybird/pipes/project_insights.pipe:15

  • The nullability reason is inaccurate: Health Score v2 and lifecycle are repository-derived and can be NULL because usable repository signals are unavailable; only impact depends on linked package data. Documenting all five fields as package-dependent obscures the public API contract.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the project has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/project_repo_insights.pipe:14

  • The nullability reason is inaccurate: Health Score v2 and lifecycle are computed from repository signals, while only impact requires linked package data. This public endpoint description should distinguish those cases.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the record has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/health_score_v2_maintainer.pipe:48

  • responsivenessAvailable removes the 15-point weight from the denominator for excluded repositories, but rawScore still adds responsivenessScore unconditionally. An excluded repo with response data can therefore receive those blocked points and then have them amplified by rescaling. Multiply the score by its availability flag, matching the other blocked sub-signals.
                (busFactorScore + orgDiversityScore + responsivenessScore) AS rawScore,
                (
                    busFactorAvailable * 18 + orgDiversityAvailable * 7 + responsivenessAvailable * 15
                ) AS coveredWeight

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:24

  • This states that graceful degradation is not implemented, but health_score_v2.pipe:51-60 explicitly rescales over available category weights and returns NULL only when all categories are unavailable. Update the datasource contract so it does not contradict the value semantics.
    - No graceful-degradation/signal-coverage redistribution yet — a repo with zero signal in a category
    scores 0 there rather than having points redistributed from available categories.

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:108

  • The live dependency result orders equal-contribution rows by id ascending when computing and returning the running total, but this rank uses memberId DESC and the serving pipe orders by this rank. For ties, the precomputed endpoint therefore returns cumulative percentages out of order and differs from the live path. Rank the already-selected top 100 using the same ascending ID tie-break as the window below.
        toUInt32(
            row_number() OVER (
                PARTITION BY ma.collectionSlug ORDER BY ma.contributionCount DESC, ma.memberId DESC
            )

Copilot AI review requested due to automatic review settings July 28, 2026 08:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (17)

services/libs/tinybird/pipes/health_score_v2.pipe:63

  • repositories is a ReplacingMergeTree, so filtering deletedAt before applying FINAL can retain an older non-deleted version after a repository is soft-deleted. That leaves deleted repositories in the final Health Score v2 copy. Query the finalized repository state before filtering, as established in repositories_populated_copy.pipe:7-8.
            FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_maintainer.pipe:149

  • This reads mutable excluded/deletedAt values from the unfinalized repositories ReplacingMergeTree. When either value changes, both historical versions can survive this DISTINCT (because excluded differs), producing multiple category rows for one URL and stale responsiveness availability. Use FINAL before filtering, consistent with repositories_populated_copy.pipe:7-8.
                                excluded AS isExcluded
                            FROM repositories
                            WHERE deletedAt IS NULL

services/libs/tinybird/pipes/health_score_v2_security.pipe:73

  • Because repositories is a ReplacingMergeTree, this pre-FINAL deletedAt filter can select an old live version of a soft-deleted repository. That repository will continue receiving a security score. Finalize the source before filtering, as other repository-derived copies do in repositories_populated_copy.pipe:7-8.
                    FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS allRepos

services/libs/tinybird/pipes/health_score_v2_development.pipe:105

  • The deletedAt predicate runs against unmerged versions of the repositories ReplacingMergeTree, so a soft-deleted repository's older row still passes and keeps a development score. Apply FINAL before this mutable-field filter, matching repositories_populated_copy.pipe:7-8.
                            FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:63

  • archived and deletedAt are mutable columns on a ReplacingMergeTree, but this query reads historical versions. An archive-state transition can therefore yield two distinct (url, archived) rows and two lifecycle labels for one repository, which then duplicates rows through the downstream joins. Read repositories FINAL before filtering, as in project_insights_copy.pipe:133-135.
            SELECT base.url AS url, base.archived AS archived, rc.lastCommitAt AS lastCommitAt
            FROM (SELECT DISTINCT url, archived FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/health_score_v2_impact.pipe:14

  • This can keep producing impact rows for soft-deleted repositories because an older deletedAt IS NULL version remains visible until FINAL is applied. Finalize the repositories ReplacingMergeTree before filtering.
    FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base

services/libs/tinybird/pipes/project_insights_copy.pipe:178

  • The emitted project score is rounded to UInt8, but the label thresholds use the unrounded average. For example, 84.6 is returned as healthScoreV2 = 85 while labeled healthy rather than the documented excellent band. Apply the thresholds to the same rounded value exposed to consumers.
        toUInt8(round(hv2.healthScoreV2)) AS healthScoreV2,
        multiIf(
            hv2.healthScoreV2 IS NULL,
            NULL,
            hv2.healthScoreV2 >= 85,

services/libs/tinybird/datasources/project_insights_copy_ds.datasource:38

  • These descriptions document the old package-based implementation, but the new query computes health and lifecycle from health_score_v2_repo_copy_ds and rolls impact up with MAX, not an average. Update the datasource contract so API consumers are not told incorrect provenance and aggregation semantics.
    - `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.
    - `healthLabel` column buckets `healthScoreV2`: 'excellent' (85+), 'healthy' (70-84), 'fair' (50-69), 'concerning' (30-49), 'critical' (<30). Lowercase, matches `ossPackages_enriched_ds.healthLabel` convention.
    - `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`.
    - `impactScore` column is the average Osprey criticality `impact` (0-100) across the project's packages. Null when the project has no linked package data.

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:15

  • This definition omits the implemented graceful-degradation rescale. When categories are unavailable, the result is not simply their sum; it is normalized by the covered category weight. Documenting it as a sum conflicts with this pipe's actual contract.
    - `healthScoreV2` (0-100) is the sum of the three categories above, clamped to 100.

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:24

  • This statement directly contradicts the newly implemented category and overall graceful degradation: unavailable signals are excluded from the denominator and available scores are redistributed. Remove the stale claim so operators do not diagnose valid rescaled scores as incorrect.
    - No graceful-degradation/signal-coverage redistribution yet — a repo with zero signal in a category
    scores 0 there rather than having points redistributed from available categories.

services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource:13

  • The security pipe treats supply-chain integrity as blocked and excludes its five points from coveredWeight; it is not hardcoded to a zero score. Correct this description because those two behaviors produce different rescaling results.
    - `securitySupplyChainScoreV2` (0-35) — open vulnerabilities, OpenSSF Scorecard, security practices,
    dependency health (checked against the repo's own published packages' dependencies, not an average
    across unrelated packages), supply chain integrity (hardcoded 0 — provenance/2FA data not yet piped).

services/libs/tinybird/pipes/project_insights.pipe:15

  • Health Score v2 and lifecycle are repo/CDP-derived and can be present without any linked package; only impact is package-mediated. This package-data nullability statement gives endpoint consumers the wrong availability contract.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the project has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/project_repo_insights.pipe:14

  • Health Score v2 and lifecycle do not depend on package linkage in this implementation; only impact does. As written, this endpoint documentation misstates when the new fields are null.
    - `healthScoreV2`, `healthLabel`, `lifecycleLabel`, `impactScore`, `impactLabel` are the Akrites-methodology fields (see `project_insights_copy_ds` for band definitions); may be null when the record has no linked package data. `healthScoreV2` is distinct from the legacy `healthScore` field.

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:3

  • This adds a separate collection contributor-dependency materialization and endpoint-routing change that is not mentioned in the Health Score v2 title, description, test plan, or deploy order. The repository workflow requires one focused feature/fix per PR (.claude/rules/commit-workflow.md:58-61); split this collection feature into its own PR so it can be validated, deployed, and rolled back independently.
DESCRIPTION >
    - `collection_contributor_dependency_copy.pipe` precomputes the DEFAULT (unfiltered) bus-factor
    result for every collection on a daily schedule, writing into `collection_contributor_dependency_copy_ds`.

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:150

  • The live member_roles node scopes roles to the requested collection via insightsProjectId IN (SELECT insightsProjectId FROM segments_filtered_by_collection) (member_roles.pipe:8-10). This materialization instead groups roles globally and joins only by member, so contributors can be shown with roles from unrelated projects or collections. Aggregate roles by both collection and member, then join on both keys.
            SELECT memberId, groupUniqArray(role) AS roles
            FROM maintainers_roles_copy_ds
            GROUP BY memberId
        ) mr
        ON mr.memberId = r.id

services/libs/tinybird/pipes/collection_contributor_dependency.pipe:107

  • This routing changes two existing request semantics: an omitted limit previously inherited the leaderboard default of 10 but is treated as 100 here, and the supported activity_types array filter is not checked at all. Such requests now take the top-100 unfiltered materialization instead of the live path. Preserve the default of 10 and route activity_types requests live.
    {% if not defined(repos) and not defined(startDate) and not defined(endDate) and not defined(
        platform
    ) and not defined(activity_type) and not Boolean(includeCollaborations, false) and Int32(
        limit, 100
    ) == 100 %} SELECT * FROM collection_contributor_dependency_precomputed

services/libs/tinybird/pipes/collection_contributor_dependency_copy.pipe:29

  • This describes the cutoff flags backwards. If the top 100 cover only 20%, every running total is <= 51, so the predicate below marks every row as isBusFactorCutoff = 1, matching the live path; it cannot produce zero flagged rows for that reason. Correct the operational note to avoid misleading validation and incident diagnosis.
    - Known, separate, and intentionally NOT fixed here: for very large collections the top-100
    ranked contributors structurally cannot reach the 51% cutoff (e.g. `cncf`'s top 100 only cover
    ~20% of total activity) - this is a product-definition question about the `limit`/cutoff
    interaction, out of scope for this performance fix. `isBusFactorCutoff` will legitimately be 0
    for every row of such a collection; the serving pipe must not assume at least one row per

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants