feat: expose health score v2 sub-signal detail via new pipe chain (CM-IN-1212) - #4448
Conversation
…-IN-1212) Promotes previously computed-but-discarded sub-signal scores and availability flags in the 3 category pipes (health_score_v2_maintainer, health_score_v2_security, health_score_v2_development), and adds a new pipe chain (health_score_v2_signal_detail -> project_insights_health_breakdown_copy -> project_insights_health_breakdown) rolling repo-level signal detail up to project level for the insights frontend's health-breakdown UI. No scoring/business logic changed, only column exposure and rollup. Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
PR SummaryMedium Risk Overview The three category scoring pipes ( A new Reviewed by Cursor Bugbot for commit 23532d1. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
Adds a Tinybird pipeline exposing Health Score v2 sub-signal details for the Insights health-breakdown UI.
Changes:
- Exposes existing category scores, availability flags, and raw metrics.
- Materializes repository-level details and project-level rollups.
- Adds a slug-filtered endpoint for Insights.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
pipes/project_insights_health_breakdown.pipe |
Adds the public breakdown endpoint. |
pipes/project_insights_health_breakdown_copy.pipe |
Aggregates repository signals by project. |
pipes/health_score_v2_signal_detail.pipe |
Joins category details by repository. |
pipes/health_score_v2_security.pipe |
Exposes security sub-signals. |
pipes/health_score_v2_maintainer.pipe |
Exposes maintainer sub-signals. |
pipes/health_score_v2_development.pipe |
Exposes development sub-signals. |
datasources/project_insights_health_breakdown_ds.datasource |
Defines project-level breakdown storage. |
datasources/health_score_v2_signal_detail_ds.datasource |
Defines repository-level detail storage. |
datasources/health_score_v2_security_ds.datasource |
Extends the security schema. |
datasources/health_score_v2_maintainer_ds.datasource |
Extends the maintainer schema. |
datasources/health_score_v2_development_ds.datasource |
Extends the development schema. |
Suppressed comments (8)
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:58
orgDiversityScoreis0for blocked repos, so averaging every row dilutes available scores with missing data whileorgDiversityAvailablestill becomes true. Average only rows whose availability flag is set.
avg(sd.orgDiversityScore) AS orgDiversityScore,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:61
- For unavailable Gerrit responsiveness data, the source emits score
0withresponsivenessAvailable = false. Including that row inavgconverts a blocked signal into a penalty for mixed-coverage projects; filter the average by availability.
avg(sd.responsivenessScore) AS responsivenessScore,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:71
- The source emits
scorecardScorePts = 0when Scorecard data is unavailable. Averaging those blocked rows lowers projects that have valid Scorecard data on only some repos; gate the average withscorecardAvailable.
avg(sd.scorecardScorePts) AS scorecardScorePts,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:74
- Missing repo-details data produces a zero security-practices score with
securityPracticesAvailable = false. This average therefore treats blocked repos as failures; exclude unavailable rows so the project score matches the exposed coverage state.
avg(sd.securityPracticesScore) AS securityPracticesScore,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:81
- When dependency data is unavailable, the source still computes a non-null fallback score while setting
dependencyHealthAvailable = false. Averaging that value makes blocked repos affect the project score; only average available rows.
avg(sd.dependencyHealthScore) AS dependencyHealthScore,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:84
- Unavailable release data yields score
0, so this average penalizes projects for blocked repos even thoughreleaseCadenceAvailablereports the signal as available when any repo has data. Gate the average on the availability flag.
avg(sd.releaseCadenceScore) AS releaseCadenceScore,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:91
- The source score is zero when issue-resolution data is blocked, so averaging all repos turns unavailable data into a negative score while
issueResolutionAvailablemay still be true. Average only available rows.
avg(sd.issueResolutionScore) AS issueResolutionScore,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:96
- The source score is zero when PR-merge data is blocked. Including those rows in the average penalizes mixed-coverage projects despite the availability flag; exclude unavailable rows from this aggregate.
avg(sd.prMergeScore) AS prMergeScore,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ 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 732f160. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (9)
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:59
- These scores are
0when their availability flag is false, so plainavgturns blocked repos into scored-zero repos while the project is marked available if any repo has data. Average only available rows and return NULL when none qualify; the same gating is needed for the other availability-backed score averages below.
avg(sd.busFactorScore) AS busFactorScore,
max(sd.busFactorAvailable) AS busFactorAvailable,
max(sd.busFactorCount) AS busFactorCount,
avg(sd.orgDiversityScore) AS orgDiversityScore,
max(sd.orgDiversityAvailable) AS orgDiversityAvailable,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:87
- These are inverse recency metrics: larger values mean an older or slower release cadence.
maxtherefore selects the stalest repository, while the consumer describes this as the project's last release; useminto select the most recent project release consistently withmax(lastCommitAt).
max(sd.daysSinceLatest) AS daysSinceLatest,
max(sd.daysBetweenRecent) AS daysBetweenRecent,
services/libs/tinybird/pipes/health_score_v2_signal_detail.pipe:67
repositoriesis a ReplacingMergeTree, so filtering mutabledeletedAtbefore resolving the latest version can retain a superseded non-deleted row after soft deletion. ApplyFINALbefore the filter, as established byrepositories_populated_copy.pipe:133andrepos_to_channels_excluded.pipe:15.
FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS base
services/libs/tinybird/pipes/project_insights_health_breakdown.pipe:13
- The production token grant described in the PR is not declared here, so a fresh workspace or redeployment will lose the manual permission and the Insights route will receive authorization errors. Persist the consuming token grant in the endpoint definition.
TAGS "Insights, Widget", "Project", "Health"
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:89
- The consumer labels this as the project's commit count, but
maxreturns only the busiest repository's count and under-reports every project with commits in multiple repos. Sum the per-repository activity counts while preserving NULL when no repository has commit history.
max(sd.commitsLast6m) AS commitsLast6m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:94
- These independent maxima can come from different repositories, so the consumer's project-level
closed / openedcomparison can describe a ratio that no aggregate dataset supports. Sum both event counts over repositories with issue-resolution data instead.
max(sd.closed12m) AS closed12m,
max(sd.opened12m) AS opened12m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:99
- Taking separate maxima can combine
merged12mfrom one repo withclosedUnmerged12mfrom another, producing an invalid project merge rate in the consumer. Sum both disjoint PR outcome counts over repositories where this signal is available.
max(sd.merged12m) AS merged12m,
max(sd.closedUnmerged12m) AS closedUnmerged12m,
services/libs/tinybird/pipes/health_score_v2_signal_detail.pipe:75
- This job remains in the already saturated 02:00 COPY window, where the account's concurrent-copy limit can leave overflow jobs queued. Move this schedule—and its downstream project rollup—into a quieter window while preserving dependency order.
COPY_SCHEDULE 25 2 * * *
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:113
- This downstream job also remains in the saturated 02:00 COPY window. Reschedule it together with
health_score_v2_signal_detailso both avoid the concurrency cap and this job still runs after its source refresh.
COPY_SCHEDULE 35 2 * * *
| max(sd.openCriticals) AS openCriticals, | ||
| max(sd.openHighs) AS openHighs, | ||
| max(sd.openModerates) AS openModerates, |
- Fix blocked-score dilution: score columns with a matching *Available flag now use sumIf/countIf coverage-filtered averaging instead of plain avg(), excluding blocked repos and preserving NULL when no repo in the project has the signal (Cursor HIGH, Copilot). - Fix min/max inversion: daysSinceLatest/daysBetweenRecent are inverse-activity durations (smaller = more recent/frequent releases per health_score_v2_development.pipe's own scoring), now min() not max() (Copilot). - Add FINAL to the repositories base-repo subquery in health_score_v2_signal_detail.pipe to avoid retaining a stale pre-soft-delete row version (Copilot). - Declare the insights-app-token READ grant directly in project_insights_health_breakdown.pipe so it survives a fresh workspace/redeploy instead of relying on a manual dashboard fix (Copilot). - Move the two new COPY pipes off the crowded 2:20-2:40am window to 50/55 2 * * *, based on real 7-day tinybird.jobs_log analysis (not a guess) showing that window hosts several long-running neighbors; the new slot lands in hour 2's emptiest observed stretch with a comfortable buffer before project_insights_copy.pipe's hard 0 3 deadline (epipav). Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (6)
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:85
- These are additive vulnerability counts, but
max()returns only the worst single repository. The consumer presents them as the project's number of open vulnerabilities, so a project with vulnerabilities in multiple repos is undercounted. Sum the nullable per-repo counts while retaining NULL when no repo has data.
max(sd.openCriticals) AS openCriticals,
max(sd.openHighs) AS openHighs,
max(sd.openModerates) AS openModerates,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:104
commitsLast6mcounts repo-specific events, so taking the maximum underreports projects with activity across multiple repositories. The consumer displays this as “N commits in the past six months” for the project; aggregate the counts instead.
max(sd.commitsLast6m) AS commitsLast6m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:109
- Taking independent maxima can combine
closed12mfrom one repo withopened12mfrom another, producing a project backlog ratio that never existed and omitting events from other repos. The consumer compares these as project-wide annual totals, so both counts should be additive.
max(sd.closed12m) AS closed12m,
max(sd.opened12m) AS opened12m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:114
- Independent maxima undercount project PR volume and can combine merged and unmerged counts from different repositories, yielding a misleading merge rate. The consumer computes and describes a project-wide ratio from these fields, so aggregate the per-repo event totals.
max(sd.merged12m) AS merged12m,
max(sd.closedUnmerged12m) AS closedUnmerged12m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:75
max()only returns the count from one repository, not the project's distinct maintainer/organization count. The Insights consumer renders these values as project-wide counts (for example, “N active maintainers”), so projects with disjoint maintainers or organizations across repositories are underreported. This needs a project-grain distinct aggregation from the underlying identities; summing repo counts would double-count identities shared across repos.
This issue also appears in the following locations of the same file:
- line 104
- line 108
- line 113
max(sd.busFactorCount) AS busFactorCount,
sumIf(sd.orgDiversityScore, sd.orgDiversityAvailable) / nullIf(countIf(sd.orgDiversityAvailable), 0) AS orgDiversityScore,
max(sd.orgDiversityAvailable) AS orgDiversityAvailable,
max(sd.orgCount) AS orgCount,
services/libs/tinybird/datasources/project_insights_health_breakdown_ds.datasource:23
- This datasource documentation says both release-duration fields use
max, but the copy pipe now intentionally usesmin(project_insights_health_breakdown_copy.pipe:101-102). Update the documented contract so consumers do not interpret these values as the stalest repository.
`daysSinceLatest`/`daysBetweenRecent` (max), `commitActivityScore` (avg), `commitsLast6m` (max),
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Suppressed comments (9)
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:77
orgCountis distinct only within each repository.max()undercounts a multi-repository project's organizational diversity when different repositories have contributors from different organizations, while the consumer presents this value as the number of organizations spanning the project. Compute a project-grain distinct organization count instead.
max(sd.orgCount) AS orgCount,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:105
vulnerableDepsis distinct only per repository, butmax()is exposed as the project's vulnerable-dependency count. Repositories with different vulnerable dependencies are therefore undercounted. Compute the distinct dependency union at project grain so shared dependencies are deduplicated without discarding dependencies unique to another repository.
max(sd.vulnerableDeps) AS vulnerableDeps,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:88
- These fields are displayed as the project's number of open vulnerabilities, but independent maxima return only the largest per-repository count. For two repositories with one critical vulnerability each, the endpoint reports 1 instead of 2; the three maxima can also originate from different repositories. Sum the per-repository counts while retaining NULL when no count data exists, and update the aggregation descriptions accordingly.
max(sd.openCriticals) AS openCriticals,
max(sd.openHighs) AS openHighs,
max(sd.openModerates) AS openModerates,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:112
- The consumer renders this as “N commits in the past six months” for the project, but
max()reports only its busiest repository. Multi-repository projects will be systematically undercounted; sum the repository counts while preserving NULL for projects without commit history, and update the aggregation descriptions.
max(sd.commitsLast6m) AS commitsLast6m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:118
- The UI calculates and describes the project-wide issue close ratio from these two fields. Taking each maximum independently both undercounts totals and can combine values from different repositories into a ratio that no repository or project has. Sum both additive counts so the displayed project ratio is accurate, preserving NULL for missing data and updating the aggregation descriptions.
max(sd.closed12m) AS closed12m,
max(sd.opened12m) AS opened12m,
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:124
- The consumer derives a project-wide PR merge rate and states “N of M pull requests” from these fields. Independent maxima can come from different repositories and do not represent the project's merged or closed-unmerged totals, producing an incorrect rate and status. Sum these additive counts across repositories, preserve NULL for missing data, and update the aggregation descriptions.
max(sd.merged12m) AS merged12m,
max(sd.closedUnmerged12m) AS closedUnmerged12m,
services/libs/tinybird/datasources/project_insights_health_breakdown_ds.datasource:23
- This datasource documents both release-duration rollups as
max, but the copy pipe now usesminfor each. Update the description so readers do not implement or debug against the obsolete aggregation contract.
`daysSinceLatest`/`daysBetweenRecent` (max), `commitActivityScore` (avg), `commitsLast6m` (max),
services/libs/tinybird/pipes/health_score_v2_signal_detail.pipe:19
project_insights_copy.pipehas no reference to this datasource or pipe; it readshealth_score_v2_repo_copy_dsinstead. The claimed transitive dependency is therefore false and can mislead future schedule changes. Describe only the actual five-minute dependency onproject_insights_health_breakdown_copy.pipe.
`project_insights_health_breakdown_copy.pipe`'s `55 2` and `project_insights_copy.pipe`'s hard
`0 3 * * *` deadline (which reads from this pipe's output transitively).
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe:56
project_insights_copy.pipedoes not read this pipe's output; it readshealth_score_v2_repo_copy_ds. Keeping a nonexistent deadline dependency in the schedule rationale may cause maintainers to preserve ordering that is not required. Remove that claim while retaining the real upstream timing rationale.
runtimes in production so the buffer is very conservative, and completes 5 minutes before
`project_insights_copy.pipe`'s hard `0 3 * * *` deadline (which reads this pipe's output).
| sumIf(sd.busFactorScore, sd.busFactorAvailable) | ||
| / nullIf(countIf(sd.busFactorAvailable), 0) AS busFactorScore, | ||
| max(sd.busFactorAvailable) AS busFactorAvailable, | ||
| max(sd.busFactorCount) AS busFactorCount, |

Summary
Part of IN-1212 (Health Score v2 content spec implementation) — the crowd.dev/Tinybird half.
*Availablecoverage flags, raw counts) in the 3 category pipes (health_score_v2_maintainer,health_score_v2_security,health_score_v2_development) — no scoring/business logic changed, only column exposure.project_insights_impact_breakdownpattern:health_score_v2_signal_detail(COPY) — LEFT JOINs the 3 widened category_dstables onrepoUrlproject_insights_health_breakdown_copy(COPY) — rolls repo-level rows up to project levelproject_insights_health_breakdown(endpoint) —slug-filtered public endpoint, consumed by the insights server routeDeploy status
Already deployed to production — validated end-to-end via
crowd-tinybird-manager(all 6 files pushed, all 5 copy backfill jobs confirmeddone,project_insights_health_breakdownconfirmed queryable by slug with real data + correct NULL-degradation behavior). Two real issues were found and fixed during the production push (not glossed over):toUInt8(...)casts.health_score_v2_maintainer_dswas missing acoveredWeightcolumn the pipe needed.insights-app-tokenalso needed aPIPES:READgrant added on the new endpoint pipe (a recurring gap worth flagging:tb pushdoesn't grant the consuming app token automatically).Staging could not be meaningfully exercised (workspace was at datasource/copy-pipe quota caps, unrelated to this change) — production deploy and validation are solid regardless.
Consumer
linuxfoundation/insightsPR (IN-1212) reads this endpoint via a newserver/api/project/[slug]/overview/health-score-breakdown.get.tsroute — that PR depends on this one already being deployed, which it is.Test plan
tb checkclean on all 11 touched filesdoneproject_insights_health_breakdownconfirmed queryable by slug with real data in productionsignal_coveragedegradation logic (Layer 1 redistribution, Layer 2 category-drop) — already implemented, this PR only exposes it