feat: align health score v2 with community feedback commitments IN-1226 - #4460
feat: align health score v2 with community feedback commitments IN-1226#4460gaspergrom wants to merge 6 commits into
Conversation
…226) Phase 1 - Issue responsiveness fix: - issue_analysis_copy_pipe: Filter comments to exclude author self-responses - This ensures 'responded' signal reflects genuine external feedback, not self-commentary Phase 2 - v2 compliance & documentation: - health_score_v2_lifecycle: Split 'abandoned' (unanswered 90-180d + no maintainer 12m) from 'inert' (no commits 18m + zero activity) - health_score_v2_maintainer: Add observed review/merge actor count (parallel to curated roster) via greatest(curated, observed) for bus-factor - health_score_v2_maintainer/lifecycle: Repoint org-diversity & bus-factor joins to use cleaned deduplicated streams (activityRelations_deduplicated_cleaned_bucket_union) instead of raw activityRelations - All v2 datasources/pipes: Add methodologyVersion='2.0.0' constant - Create health_score_v2_raw_inputs_snapshot pipe+datasource (monthly append-mode capture of raw signals for validation) - README: Document repositories.excluded as official experimental-repo mechanism - project_insights_health_breakdown_copy: Add busFactorScoreActivityWeightedMean for validation/comparison alongside existing coverage-filtered average - health_score_v2_repo_copy_ds: Fix description (graceful-degradation IS implemented, document security_contact_email spec gap as temporary) Item 7 verification: All 9 per-signal *Available flags confirmed present and emitted (busFactorAvailable, orgDiversityAvailable, responsivenessAvailable, scorecardAvailable, securityPracticesAvailable, dependencyHealthAvailable, releaseCadenceAvailable, issueResolutionAvailable, prMergeAvailable). Note: Day thresholds (90/180/18mo) are provisional pending validation analysis. No changes to IN-1212 branch. Postgres writeback deferred (Tinybird-only per decision). Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
…N-1226) - issue_analysis_copy_pipe: fix node name mismatch (issues_comment -> issues_comment_non_author) and column alias (commentedAt -> respondedAt) that would have failed at Tinybird push time - project_insights_health_breakdown_ds: add missing busFactorScoreActivityWeightedMean schema column to match the pipe output that already emitted it - health_score_v2_raw_inputs_snapshot: fix vulnerable-deps join to use the real advisory schema (packageDependencies/advisoryPackages/advisories), matching the pattern already proven in health_score_v2_security.pipe -- the pipe previously referenced a nonexistent 'dependencies' table Found while dispatching the Tinybird deploy for the IN-1226 commit; 8 of 9 pipes and all datasources deployed clean on the first pass, this fixes the one that didn't. Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
…1226)
health_score_v2_raw_inputs_snapshot.pipe previously split its base data
into three NODEs (maintainer/security/development) joined together in a
final calc node. Tinybird cannot resolve column references once it inlines
multiple independently-defined subqueries together at that depth
("Identifier 's.repoUrl' cannot be resolved from subquery with name s").
The three proven, already-deployed category pipes (health_score_v2_maintainer/
_security/_development) never join across NODEs this way - each does its
base + joins in a single node. Collapsed this pipe to match that pattern.
Also fixes 13 SELECT aliases that didn't match the target datasource's
schema column names (e.g. curatedBusFactorCount vs busFactorCuratedCount) -
same class of reference bug as 9c0e94d, only surfaces at actual deploy.
Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
quantile(0.5)(dateDiff(..., max(...) OVER (PARTITION BY ...))) is rejected by ClickHouse - window functions can't nest inside an aggregate. Replaced with the same groupArray/arraySort/arraySlice top-2-dates approach already proven in health_score_v2_development.pipe's release-cadence subquery. Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
… (IN-1226) Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
…N-1226) The vulnerabilities datasource has always stored medium-severity CVEs as severity='MEDIUM' (see extractSeverity.ts), but health_score_v2_security and health_score_v2_raw_inputs_snapshot filtered on severity='MODERATE', a value that never existed in the data. openModerateVulns/openModerates were silently 0 for every repo, so the openVulnScore penalty term for medium-severity vulnerabilities never applied, inflating securitySupplyChainScoreV2 for any repo with real medium-severity CVEs. Found via the Part 3 CVE data sanity validation analysis (0/31,710 repos ever showed a moderate CVE, which is not a plausible population characteristic). Also corrects the same stale MODERATE reference in the vulnerabilities datasource's severity enum docstring. Signed-off-by: Gašper Grom <gasper.grom@gmail.com>
PR SummaryMedium Risk Overview Scoring and upstream fixes: Maintainer bus factor uses Docs: README documents Reviewed by Cursor Bugbot for commit 310860e. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
❌ 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 310860e. Configure here.
| (r.lastCommitAt < now() - INTERVAL 18 MONTH) | ||
| AND coalesce(w.issuesInWindow18m, 0) + coalesce(p.prsInWindow18m, 0) = 0, | ||
| 'abandoned', | ||
| 'inert', |
There was a problem hiding this comment.
Inert label breaks project rollup
High Severity
The new inert lifecycle label is emitted by health_score_v2_lifecycle.pipe, but project_insights_copy.pipe's best-state-wins indexOf list still only has active/stable/declining/abandoned/archived. ClickHouse returns 0 for a missing value, so inert sorts ahead of every other state and can override active at project level.
Reviewed by Cursor Bugbot for commit 310860e. Configure here.
| coalesce(w.issuesInWindow18m, 0) > 0 | ||
| AND coalesce(w.unansweredCount, 0) > 0 | ||
| AND (r.lastCommitAt < now() - INTERVAL 12 MONTH), | ||
| 'abandoned', |
There was a problem hiding this comment.
Abandoned misses NULL lastCommitAt
Medium Severity
The new abandoned branch requires lastCommitAt < now() - INTERVAL 12 MONTH, but this pipe already documents that repos.lastCommitAt is often NULL as an upstream data gap. In ClickHouse that comparison is false for NULL, so repos with unanswered issues and no recent commits can still fall through to active.
Reviewed by Cursor Bugbot for commit 310860e. Configure here.
| sd.busFactorAvailable | ||
| ) / nullIf( | ||
| sumIf(coalesce(sd.commitsLast6m, 1), sd.busFactorAvailable), 0 | ||
| ) AS busFactorScoreActivityWeightedMean |
There was a problem hiding this comment.
Weighted mean coalesce asymmetry
Medium Severity
busFactorScoreActivityWeightedMean uses coalesce(commitsLast6m, 0) in the numerator and coalesce(commitsLast6m, 1) in the denominator. Repos with busFactorAvailable but NULL commitsLast6m add weight without contributing score, which dilutes the activity-weighted mean.
Reviewed by Cursor Bugbot for commit 310860e. Configure here.
| p.merged12m AS prsMergedLast12m, | ||
| p.closedUnmerged12m AS prsClosedUnmergedLast12m, | ||
| p.medianMergeS AS medianPrMergeSeconds | ||
| FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS allRepos |
There was a problem hiding this comment.
New pipe omits FINAL keyword
Low Severity
The new snapshot pipe reads repositories with SELECT DISTINCT ... WHERE deletedAt IS NULL and no FINAL. repositories is a ReplacingMergeTree, so unmerged soft-delete versions can still surface without FINAL.
Triggered by learned rule: Tinybird pipes querying ReplacingMergeTree datasources must use FINAL keyword
Reviewed by Cursor Bugbot for commit 310860e. Configure here.
There was a problem hiding this comment.
Pull request overview
Aligns Health Score v2 Tinybird pipelines with IN-1226 requirements.
Changes:
- Adds lifecycle, bus-factor, responsiveness, and vulnerability corrections.
- Introduces methodology versioning and monthly raw-input snapshots.
- Documents repository exclusion and adds weighted bus-factor aggregation.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
services/libs/tinybird/README.md |
Documents repository exclusion. |
services/libs/tinybird/pipes/project_insights_health_breakdown_copy.pipe |
Adds activity-weighted bus-factor aggregation. |
services/libs/tinybird/pipes/issue_analysis_copy_pipe.pipe |
Measures non-author issue responses. |
services/libs/tinybird/pipes/health_score_v2.pipe |
Adds methodology version. |
services/libs/tinybird/pipes/health_score_v2_signal_detail.pipe |
Exposes methodology version. |
services/libs/tinybird/pipes/health_score_v2_security.pipe |
Corrects medium severity and versions output. |
services/libs/tinybird/pipes/health_score_v2_raw_inputs_snapshot.pipe |
Adds monthly raw-input snapshots. |
services/libs/tinybird/pipes/health_score_v2_maintainer.pipe |
Adds observed-actor bus-factor fallback. |
services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe |
Splits abandoned and inert states. |
services/libs/tinybird/pipes/health_score_v2_development.pipe |
Adds methodology version. |
services/libs/tinybird/datasources/vulnerabilities.datasource |
Corrects severity documentation. |
services/libs/tinybird/datasources/project_insights_health_breakdown_ds.datasource |
Stores weighted bus-factor metric. |
services/libs/tinybird/datasources/health_score_v2_signal_detail_ds.datasource |
Stores methodology version. |
services/libs/tinybird/datasources/health_score_v2_security_ds.datasource |
Stores methodology version. |
services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource |
Updates scoring documentation and schema. |
services/libs/tinybird/datasources/health_score_v2_raw_inputs_snapshot_ds.datasource |
Defines snapshot storage. |
services/libs/tinybird/datasources/health_score_v2_maintainer_ds.datasource |
Stores methodology version. |
services/libs/tinybird/datasources/health_score_v2_lifecycle_ds.datasource |
Stores lifecycle methodology version. |
services/libs/tinybird/datasources/health_score_v2_development_ds.datasource |
Stores methodology version. |
Suppressed comments (2)
services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe:62
- Introducing
inerthere leaves the project rollup unaware of the new value.project_insights_copy.pipe:130usesindexOf(['active', 'stable', 'declining', 'abandoned', 'archived'], x);indexOfreturns 0 forinert, so a mixed active/inert project incorrectly rolls up toinertinstead of the best state. Addinertbetweendecliningandabandonedin that ordering and update the associated lifecycle descriptions.
'inert',
services/libs/tinybird/pipes/health_score_v2_raw_inputs_snapshot.pipe:220
- These PR inputs also diverge from
health_score_v2_development.pipe:216-225: old PRs merged/closed recently are included and the median spans all history, whereas scoring starts with PRs opened in the trailing 12 months. Use the same window as the category pipe so snapshots remain auditable.
countIf(mergedAt IS NOT NULL AND mergedAt > now() - INTERVAL 12 MONTH) AS merged12m,
countIf(
closedAt IS NOT NULL AND mergedAt IS NULL AND closedAt > now() - INTERVAL 12 MONTH
) AS closedUnmerged12m,
quantile(0.5)
(mergedInSeconds) AS medianMergeS
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| (r.lastCommitAt < now() - INTERVAL 18 MONTH) | ||
| AND coalesce(w.issuesInWindow18m, 0) + coalesce(p.prsInWindow18m, 0) = 0, | ||
| 'abandoned', | ||
| 'inert', |
| countIf( | ||
| openedAt > now() - INTERVAL 18 MONTH AND respondedInSeconds IS NULL | ||
| ) AS unansweredCount |
| WHERE | ||
| timestamp > now() - INTERVAL 12 MONTH | ||
| AND ( | ||
| type = 'pull_request-reviewed' | ||
| OR type = 'pull_request-merged' | ||
| OR type = 'merge_request-review-approved' | ||
| OR type = 'merge_request-merged' | ||
| ) |
| WHERE | ||
| timestamp > now() - INTERVAL 12 MONTH | ||
| AND ( | ||
| type = 'pull_request-reviewed' | ||
| OR type = 'pull_request-merged' | ||
| OR type = 'merge_request-review-approved' | ||
| OR type = 'merge_request-merged' | ||
| ) |
| c.type = 'issue-comment' | ||
| AND c.sourceParentId != '' | ||
| AND c.memberId != opened.memberId | ||
| AND toYear(c.timestamp) >= 1971 |
| repoMeta.branchProtectionRequiredReviews, | ||
| repoMeta.branchProtectionRequiresStatusChecks, | ||
| repoMeta.branchProtectionAllowsForcePush, | ||
| deps.vulnerableDeps, |
| p.merged12m AS prsMergedLast12m, | ||
| p.closedUnmerged12m AS prsClosedUnmergedLast12m, | ||
| p.medianMergeS AS medianPrMergeSeconds | ||
| FROM (SELECT DISTINCT url AS repoUrl FROM repositories WHERE deletedAt IS NULL) AS allRepos |
| ENGINE MergeTree | ||
| ENGINE_SORTING_KEY (snapshotDate, repoUrl) | ||
| ENGINE_TTL snapshotDate + INTERVAL 24 MONTH |
| UPDATE repositories SET excluded = 1 WHERE url = 'https://github.com/org/repo-meta'; | ||
| ``` | ||
|
|
||
| The flag is read by `health_score_v2_maintainer.pipe` and `health_score_v2_lifecycle.pipe` as part of their graceful-degradation logic. |
| coalesce(sd.commitsLast6m, 0) * coalesce(sd.busFactorScore, 0), | ||
| sd.busFactorAvailable | ||
| ) / nullIf( | ||
| sumIf(coalesce(sd.commitsLast6m, 1), sd.busFactorAvailable), 0 |


Summary
JIRA
IN-1226