diff --git a/services/libs/tinybird/datasources/health_score_v2_development_ds.datasource b/services/libs/tinybird/datasources/health_score_v2_development_ds.datasource new file mode 100644 index 0000000000..b93e61bf26 --- /dev/null +++ b/services/libs/tinybird/datasources/health_score_v2_development_ds.datasource @@ -0,0 +1,13 @@ +DESCRIPTION > + - `health_score_v2_development_ds` holds the per-repo Development Activity category score (0-25) + for Health Score v2. Populated by `health_score_v2_development.pipe`. + - `repoUrl` is the repository URL — the join key back to `repositories`. + - `developmentActivityScoreV2` — NULL when covered sub-signal weight is <40% of the 25pt max + (spec Layer 1 graceful degradation), otherwise the rescaled 0-25 score. + +SCHEMA > + `repoUrl` String, + `developmentActivityScoreV2` Nullable(UInt8) + +ENGINE MergeTree +ENGINE_SORTING_KEY repoUrl diff --git a/services/libs/tinybird/datasources/health_score_v2_impact_ds.datasource b/services/libs/tinybird/datasources/health_score_v2_impact_ds.datasource new file mode 100644 index 0000000000..c238839cf4 --- /dev/null +++ b/services/libs/tinybird/datasources/health_score_v2_impact_ds.datasource @@ -0,0 +1,13 @@ +DESCRIPTION > + - `health_score_v2_impact_ds` holds the per-repo raw Impact Score (0-100 float, pre-rounding) + for Health Score v2. Populated by `health_score_v2_impact.pipe`. + - `repoUrl` is the repository URL — the join key back to `repositories`. + - `impactScoreRaw` — MAX(packages.impact) * 100 over packages published by this repo. NULL when + the repo has no linked packages with a known impact value. + +SCHEMA > + `repoUrl` String, + `impactScoreRaw` Nullable(Float64) + +ENGINE MergeTree +ENGINE_SORTING_KEY repoUrl diff --git a/services/libs/tinybird/datasources/health_score_v2_lifecycle_ds.datasource b/services/libs/tinybird/datasources/health_score_v2_lifecycle_ds.datasource new file mode 100644 index 0000000000..4c0f6b220e --- /dev/null +++ b/services/libs/tinybird/datasources/health_score_v2_lifecycle_ds.datasource @@ -0,0 +1,14 @@ +DESCRIPTION > + - `health_score_v2_lifecycle_ds` holds the per-repo Lifecycle state for Health Score v2. + Populated by `health_score_v2_lifecycle.pipe`. + - `repoUrl` is the repository URL — the join key back to `repositories`. + - `lifecycleLabelV2` — one of active/stable/declining/abandoned/archived, or NULL when the + repo has no usable activity signal at all (2026-07-24, IN-1196 — see + health_score_v2_lifecycle.pipe for the exact condition). + +SCHEMA > + `repoUrl` String, + `lifecycleLabelV2` Nullable(String) + +ENGINE MergeTree +ENGINE_SORTING_KEY repoUrl diff --git a/services/libs/tinybird/datasources/health_score_v2_maintainer_ds.datasource b/services/libs/tinybird/datasources/health_score_v2_maintainer_ds.datasource new file mode 100644 index 0000000000..63fe235610 --- /dev/null +++ b/services/libs/tinybird/datasources/health_score_v2_maintainer_ds.datasource @@ -0,0 +1,13 @@ +DESCRIPTION > + - `health_score_v2_maintainer_ds` holds the per-repo Maintainer Health category score (0-40) + for Health Score v2. Populated by `health_score_v2_maintainer.pipe`. + - `repoUrl` is the repository URL — the join key back to `repositories`. + - `maintainerHealthScoreV2` — NULL when covered sub-signal weight is <40% of the 40pt max + (spec Layer 1 graceful degradation), otherwise the rescaled 0-40 score. + +SCHEMA > + `repoUrl` String, + `maintainerHealthScoreV2` Nullable(UInt8) + +ENGINE MergeTree +ENGINE_SORTING_KEY repoUrl diff --git a/services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource b/services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource new file mode 100644 index 0000000000..55ff72956f --- /dev/null +++ b/services/libs/tinybird/datasources/health_score_v2_repo_copy_ds.datasource @@ -0,0 +1,36 @@ +DESCRIPTION > + - `health_score_v2_repo_copy_ds` contains the per-repo Akrites Health Score v2 breakdown. + - Populated by `health_score_v2.pipe` copy pipe. + - Health Score v2 is computed entirely from repo-level/CDP data (maintainer activity, security posture, + development activity) — no package data, per the Health Score v2 methodology + (https://linuxfoundation-dxwx.dsp.so/4ezMyw7n-insights-projects-new-scores). Impact Score is the + one exception — package-mediated by design, computed here as MAX(packages.impact) over the repo's + own published packages (via packageRepos), not an average over unrelated packages. + - `repoUrl` is the repository URL — the join key back to `repositories`/`repos`. + - `maintainerHealthScoreV2` (0-40) — bus factor, org diversity, maintainer responsiveness. + - `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). + - `developmentActivityScoreV2` (0-25) — release cadence, commit activity, issue resolution, PR merge health. + - `healthScoreV2` (0-100) is the sum of the three categories above, clamped to 100. + - `lifecycleLabelV2` — per-repo lifecycle state (active/stable/declining/abandoned/archived), computed + via the spec's decision tree (archived flag > abandoned > declining > stable > active, first match + wins), or NULL when the repo has zero commits and zero issues/PRs in every window checked (2026-07-24, + IN-1196 — no usable activity signal at all). Project-level rollup uses best-state-wins precedence in + project_insights_copy.pipe. + - `impactScore` (0-100) — MAX(packages.impact) * 100 over packages published by this repo. NULL when + the repo has no linked packages. + - 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. + +SCHEMA > + `repoUrl` String, + `maintainerHealthScoreV2` Nullable(UInt8), + `securitySupplyChainScoreV2` Nullable(UInt8), + `developmentActivityScoreV2` Nullable(UInt8), + `healthScoreV2` Nullable(UInt8), + `lifecycleLabelV2` Nullable(String), + `impactScore` Nullable(UInt8) + +ENGINE MergeTree +ENGINE_SORTING_KEY repoUrl diff --git a/services/libs/tinybird/datasources/health_score_v2_security_ds.datasource b/services/libs/tinybird/datasources/health_score_v2_security_ds.datasource new file mode 100644 index 0000000000..b049414c74 --- /dev/null +++ b/services/libs/tinybird/datasources/health_score_v2_security_ds.datasource @@ -0,0 +1,13 @@ +DESCRIPTION > + - `health_score_v2_security_ds` holds the per-repo Security & Supply Chain category score (0-35) + for Health Score v2. Populated by `health_score_v2_security.pipe`. + - `repoUrl` is the repository URL — the join key back to `repositories`. + - `securitySupplyChainScoreV2` — NULL when covered sub-signal weight is <40% of the 35pt max + (spec Layer 1 graceful degradation), otherwise the rescaled 0-35 score. + +SCHEMA > + `repoUrl` String, + `securitySupplyChainScoreV2` Nullable(UInt8) + +ENGINE MergeTree +ENGINE_SORTING_KEY repoUrl diff --git a/services/libs/tinybird/datasources/project_insights_copy_ds.datasource b/services/libs/tinybird/datasources/project_insights_copy_ds.datasource index bcc867c41c..fde0a8fd52 100644 --- a/services/libs/tinybird/datasources/project_insights_copy_ds.datasource +++ b/services/libs/tinybird/datasources/project_insights_copy_ds.datasource @@ -32,6 +32,11 @@ DESCRIPTION > - `activeContributorsPrevious365Days` column is the unique count of active contributors in the previous 365 days (365-730 days ago). - `activeOrganizationsPrevious365Days` column is the unique count of active organizations in the previous 365 days (365-730 days ago). - `status` column is the current status of the project (e.g., 'active', 'archived'). + - `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. TAGS "Project insights", "Metrics" @@ -65,7 +70,12 @@ SCHEMA > `starsPrevious365Days` UInt64, `forksPrevious365Days` UInt64, `activeContributorsPrevious365Days` UInt64, - `activeOrganizationsPrevious365Days` UInt64 + `activeOrganizationsPrevious365Days` UInt64, + `healthScoreV2` Nullable(UInt8), + `healthLabel` Nullable(String), + `lifecycleLabel` Nullable(String), + `impactScore` Nullable(UInt8), + `impactLabel` Nullable(String) ENGINE MergeTree ENGINE_SORTING_KEY type, id diff --git a/services/libs/tinybird/pipes/health_score_v2.pipe b/services/libs/tinybird/pipes/health_score_v2.pipe new file mode 100644 index 0000000000..c7605fef12 --- /dev/null +++ b/services/libs/tinybird/pipes/health_score_v2.pipe @@ -0,0 +1,63 @@ +DESCRIPTION > + - Combines the 5 independently-materialized Health Score v2 category/label datasources into the + final per-repo row: Maintainer Health (40) + Security & Supply Chain (35) + Development + Activity (25) + Lifecycle label + Impact Score, per the Health Score v2 methodology + (https://linuxfoundation-dxwx.dsp.so/4ezMyw7n-insights-projects-new-scores). + - Split into 5 category pipes (health_score_v2_maintainer/security/development/lifecycle/impact.pipe) + on 2026-07-22, each with its own COPY_SCHEDULE and small materialized *_ds datasource, because + the previous single-pipe 6-node version re-inlined all upstream NODEs into one query per copy + job — Tinybird does not materialize intermediate NODEs, so `repositories` and other base tables + were being re-scanned once per category, and the 6-node version's runtime grew from ~13min to + 20-28+ minutes across repeated runs even with an equivalent-cost SQL change (see git history). + This node now does a single cheap LEFT JOIN over 5 small pre-computed tables instead. + - Applies spec Layer 2 graceful degradation across categories: healthScoreV2 = SUM(available + category subtotals) * (100 / SUM(available category max weights)) — each category datasource + already carries NULL when its own coverage fell below 40% (Layer 1, done per-category pipe). + If all three categories are unavailable, healthScoreV2 itself becomes NULL, per spec, rather + than a fabricated 0. Impact stays NULL (not defaulted to 0) when the repo has no packages. + - lifecycleLabelV2 passes through health_score_v2_lifecycle_ds unmodified (2026-07-24, + IN-1196) — previously wrapped in `coalesce(l.lifecycleLabelV2, 'active')`, which silently + converted the upstream pipe's genuine "no usable signal" NULL back into a fabricated 'active' + before it ever reached health_score_v2_repo_copy_ds. Confirmed live that this LEFT JOIN has + zero actual join-misses (every repo has a matching lifecycle row), so the coalesce was never + serving its ostensible "missing row" fallback purpose in practice — it was only erasing real + NULLs. + +NODE health_score_v2_results +SQL > + SELECT + repoUrl, + maintainerHealthScoreV2, + securitySupplyChainScoreV2, + developmentActivityScoreV2, + if( + coveredCategoryWeight = 0, + NULL, + toNullable(toUInt8(round(least(100.0, availableCategorySum * (100.0 / coveredCategoryWeight))))) + ) AS healthScoreV2, + lifecycleLabelV2, + if(impactScoreRaw IS NULL, NULL, toNullable(toUInt8(round(least(impactScoreRaw, 100))))) AS impactScore + FROM ( + SELECT + base.repoUrl AS repoUrl, + m.maintainerHealthScoreV2 AS maintainerHealthScoreV2, + s.securitySupplyChainScoreV2 AS securitySupplyChainScoreV2, + d.developmentActivityScoreV2 AS developmentActivityScoreV2, + (coalesce(m.maintainerHealthScoreV2,0) + coalesce(s.securitySupplyChainScoreV2,0) + coalesce(d.developmentActivityScoreV2,0)) AS availableCategorySum, + (if(m.maintainerHealthScoreV2 IS NOT NULL, 40, 0) + + if(s.securitySupplyChainScoreV2 IS NOT NULL, 35, 0) + + 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 + LEFT JOIN health_score_v2_maintainer_ds AS m ON m.repoUrl = base.repoUrl + LEFT JOIN health_score_v2_security_ds AS s ON s.repoUrl = base.repoUrl + LEFT JOIN health_score_v2_development_ds AS d ON d.repoUrl = base.repoUrl + LEFT JOIN health_score_v2_lifecycle_ds AS l ON l.repoUrl = base.repoUrl + LEFT JOIN health_score_v2_impact_ds AS i ON i.repoUrl = base.repoUrl + ) + +TYPE COPY +TARGET_DATASOURCE health_score_v2_repo_copy_ds +COPY_MODE replace +COPY_SCHEDULE 30 2 * * * diff --git a/services/libs/tinybird/pipes/health_score_v2_development.pipe b/services/libs/tinybird/pipes/health_score_v2_development.pipe new file mode 100644 index 0000000000..87f5b50065 --- /dev/null +++ b/services/libs/tinybird/pipes/health_score_v2_development.pipe @@ -0,0 +1,108 @@ +DESCRIPTION > + Development Activity category (max 25 pts): release cadence (8), commit activity (5), issue + resolution (7), PR merge health (5). Release cadence uses distinct release DAYS (not raw + publishedAt timestamps) because monorepo projects publish many packages within seconds of each + other, which would otherwise read as near-zero days between releases. Commit activity counts + `authored-commit` across all platforms (not just platform='git') since GitHub-sourced commits are + tagged platform='github', not 'git', in activityRelations. Falls back to repos.lastCommitAt + (4 pts if <3mo, 2 if <12mo, else 0) when a repo has zero rows in activityRelations at all — + per spec, and because only ~30K of 3.3M repos have any git activity ingested, vs. ~6M rows + (most repos) having lastCommitAt from GitHub enrichment — the fallback covers the vast majority. + - Split out of health_score_v2.pipe into its own copy pipe (2026-07-22) — see + health_score_v2_maintainer.pipe description for why. + +NODE health_score_v2_development_calc +SQL > + SELECT + repoUrl, + if(coveredWeight / 25.0 < 0.4, NULL, toNullable(toUInt8(round(least(25.0, rawScore * (25.0 / coveredWeight)))))) AS developmentActivityScoreV2 + FROM ( + SELECT + repoUrl, + (releaseCadenceAvailable * releaseCadenceScore + commitActivityScore + + issueResolutionAvailable * issueResolutionScore + prMergeAvailable * prMergeScore) AS rawScore, + (releaseCadenceAvailable * 8 + 5 + issueResolutionAvailable * 7 + prMergeAvailable * 5) AS coveredWeight + FROM ( + SELECT + rp.repoUrl AS repoUrl, + (rs.repoUrl != '') AS releaseCadenceAvailable, + multiIf( + rs.daysSinceLatest < 90 AND rs.daysBetweenRecent < 90, 8, + rs.daysSinceLatest < 180, 6, + rs.daysSinceLatest < 365, 4, + rs.daysSinceLatest < 730, 2, + 0 + ) AS releaseCadenceScore, + -- commit activity is never `blocked`: it has its own spec-mandated fallback to + -- repos.lastCommitAt, so it always contributes its full 5pt weight to coverage + multiIf( + c.repoUrl != '', + multiIf(c.commitsLast6m >= 50, 5, c.commitsLast6m >= 20, 4, c.commitsLast6m >= 5, 3, c.commitsLast6m >= 1, 1, 0), + rp.lastCommitAt > now() - INTERVAL 3 MONTH, 4, + rp.lastCommitAt > now() - INTERVAL 12 MONTH, 2, + 0 + ) AS commitActivityScore, + (ist.repoUrl != '') AS issueResolutionAvailable, + (multiIf(ist.opened12m = 0, 0, (ist.closed12m / ist.opened12m) >= 0.8, 4, (ist.closed12m / ist.opened12m) >= 0.5, 2, 0) + + multiIf(ist.medianCloseS < 604800, 3, ist.medianCloseS < 2592000, 2, 0)) AS issueResolutionScore, + (prs.repoUrl != '') AS prMergeAvailable, + (multiIf((prs.merged12m + prs.closedUnmerged12m) = 0, 0, (prs.merged12m / (prs.merged12m + prs.closedUnmerged12m)) >= 0.7, 3, (prs.merged12m / (prs.merged12m + prs.closedUnmerged12m)) >= 0.4, 1, 0) + + 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 + LEFT JOIN ( + SELECT url, argMax(lastCommitAt, updatedAt) AS lastCommitAt + FROM repos GROUP BY url + ) AS rc ON rc.url = base.url + ) AS rp + LEFT JOIN ( + SELECT + repoUrl, + dateDiff('day', top2[1], today()) AS daysSinceLatest, + if(length(top2) >= 2, dateDiff('day', top2[2], top2[1]), 9999) AS daysBetweenRecent + FROM ( + SELECT + r.url AS repoUrl, + arraySlice(arraySort(x -> -toInt64(x), arrayDistinct(groupArray(rd.releaseDay))), 1, 2) AS top2 + FROM repos r + INNER JOIN packageRepos pr ON pr.repoId = r.id + INNER JOIN ( + SELECT DISTINCT packageId, toDate(publishedAt) AS releaseDay + FROM versions WHERE publishedAt IS NOT NULL + ) rd ON rd.packageId = pr.packageId + GROUP BY r.url + ) + ) AS rs ON rs.repoUrl = rp.repoUrl + LEFT JOIN ( + SELECT channel AS repoUrl, + countIf(timestamp > now() - INTERVAL 6 MONTH) AS commitsLast6m + FROM activityRelations_deduplicated_cleaned_bucket_union + WHERE type = 'authored-commit' + GROUP BY channel + ) AS c ON c.repoUrl = rp.repoUrl + LEFT JOIN ( + SELECT channel AS repoUrl, + countIf(closedAt IS NOT NULL) AS closed12m, + count() AS opened12m, + quantileIf(0.5)(closedInSeconds, closedAt > now() - INTERVAL 12 MONTH) AS medianCloseS + FROM issues_analyzed + WHERE openedAt > now() - INTERVAL 12 MONTH + GROUP BY channel + ) AS ist ON ist.repoUrl = rp.repoUrl + LEFT JOIN ( + SELECT channel AS repoUrl, + countIf(mergedAt IS NOT NULL) AS merged12m, + countIf(closedAt IS NOT NULL AND mergedAt IS NULL) AS closedUnmerged12m, + quantileIf(0.5)(mergedInSeconds, mergedAt > now() - INTERVAL 12 MONTH) AS medianMergeS + FROM pull_requests_analyzed + WHERE openedAt > now() - INTERVAL 12 MONTH + GROUP BY channel + ) AS prs ON prs.repoUrl = rp.repoUrl + ) + ) + +TYPE COPY +TARGET_DATASOURCE health_score_v2_development_ds +COPY_MODE replace +COPY_SCHEDULE 10 2 * * * diff --git a/services/libs/tinybird/pipes/health_score_v2_impact.pipe b/services/libs/tinybird/pipes/health_score_v2_impact.pipe new file mode 100644 index 0000000000..ccd8b5a01d --- /dev/null +++ b/services/libs/tinybird/pipes/health_score_v2_impact.pipe @@ -0,0 +1,25 @@ +DESCRIPTION > + Impact Score (max 100, package-mediated — the one score meant to depend on packages) = + ROUND(100 * MAX(packages.impact)) over packages published by this repo, via + repos -> packageRepos -> ossPackages_enriched_ds. Per spec this is MAX (not AVG) and joins via + packageRepos/repos (repo's own published packages), NOT packageDownloads (a looser, + undocumented linkage that caused the same averaging bug in Health Score v2 before it was fixed + to be repo-derived). NULL when the repo has no linked packages with a known impact value. + - Split out of health_score_v2.pipe into its own copy pipe (2026-07-22) — see + health_score_v2_maintainer.pipe description for why. + +NODE health_score_v2_impact_calc +SQL > + SELECT + base.url AS repoUrl, + max(toFloat64OrNull(pk.impact)) * 100 AS impactScoreRaw + FROM (SELECT DISTINCT url FROM repositories WHERE deletedAt IS NULL) AS base + INNER JOIN repos r ON r.url = base.url + INNER JOIN packageRepos pr ON pr.repoId = r.id + INNER JOIN ossPackages_enriched_ds pk ON pk.id = pr.packageId + GROUP BY base.url + +TYPE COPY +TARGET_DATASOURCE health_score_v2_impact_ds +COPY_MODE replace +COPY_SCHEDULE 20 2 * * * diff --git a/services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe b/services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe new file mode 100644 index 0000000000..7cc9cc2883 --- /dev/null +++ b/services/libs/tinybird/pipes/health_score_v2_lifecycle.pipe @@ -0,0 +1,111 @@ +DESCRIPTION > + Per-repo Lifecycle state, computed via the spec's decision tree (first match wins): + archived (repos.archived) > abandoned (no commits in 18mo AND no issue/PR activity in 18mo) > + declining (commits down >50% vs prior 6mo AND issues opened up vs prior 6mo) > stable (release + within 12mo, <50 open issues, no open critical vulns, commits down >50% vs prior 6mo) > active + (fallback) — but NULL overrides all of the above when there is no usable signal at all (see + below). Project-level rollup (best-state-wins) happens in project_insights_copy.pipe. + - Split out of health_score_v2.pipe into its own copy pipe (2026-07-22) — see + health_score_v2_maintainer.pipe description for why. + - `NULL` branch added 2026-07-24 (IN-1196, per Joana's review): the original spec's `active` + fallback fired whenever a repo failed every other branch, including repos with NO real signal + at all. Confirmed on blocknetdx/blocknet: lastCommitAt IS NULL and commits/issues/PRs are all + exactly 0 in every window — nothing here indicates the repo is active, it was defaulting to + 'active' purely because no other branch happened to match. + This branch now returns NULL (not a fabricated 'active', and not a literal 'unavailable' + string either — kept consistent with how `healthScoreV2` itself already represents + insufficient signal) for repos with zero commit activity (both 6-month windows) AND zero + issues/PRs opened in the 18-month window — i.e. every activity signal this pipe looks at is + exactly zero, not just missing one field. The UI layer (insights repo) is responsible for + rendering NULL as an "Unavailable" chip, same as it already does for a missing healthScoreV2. + `lastCommitAt IS NULL` alone does NOT trigger this branch: GravityView and Hitomi-Downloader + both have `repos.lastCommitAt` unset (a separate, upstream data gap in the `repos` table) but + have substantial real issue/PR activity (108-762 issues, 12-162 PRs in the 18mo window) — they + correctly still resolve via their actual signal (falling through to 'active', same as before + this fix) rather than being misclassified as unavailable just because one timestamp field is + missing. bminor/binutils-gdb similarly keeps falling through to 'active' despite + commitsLast6m=0 (down from 1895 prior), because it still has 3 real PRs in the 18mo window — + real activity, just not enough to clear any of the other branches' thresholds. That's a + legitimate 'active' classification under the existing spec, not the bug being fixed here. + +NODE health_score_v2_lifecycle_calc +SQL > + SELECT + r.url AS repoUrl, + if( + r.archived != 1 + AND coalesce(c.commitsLast6m, 0) = 0 + AND coalesce(c.commitsPrior6m, 0) = 0 + AND coalesce(w.issuesInWindow18m, 0) + coalesce(p.prsInWindow18m, 0) = 0, + NULL, + toNullable(multiIf( + r.archived = 1, + 'archived', + (r.lastCommitAt < now() - INTERVAL 18 MONTH) + AND coalesce(w.issuesInWindow18m, 0) + coalesce(p.prsInWindow18m, 0) = 0, + 'abandoned', + coalesce(c.commitsLast6m, 0) < coalesce(c.commitsPrior6m, 0) * 0.5 + AND coalesce(w.issuesOpenedLast6m, 0) > coalesce(w.issuesOpenedPrior6m, 0), + 'declining', + (rl.latestReleaseDay > today() - 365) + AND coalesce(n.issuesOpenNow, 0) < 50 + AND coalesce(v.openCriticals, 0) = 0 + AND coalesce(c.commitsLast6m, 0) < coalesce(c.commitsPrior6m, 0) * 0.5, + 'stable', + 'active' + )) + ) AS lifecycleLabelV2 + FROM ( + 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 + LEFT JOIN ( + SELECT url, argMax(lastCommitAt, updatedAt) AS lastCommitAt + FROM repos GROUP BY url + ) AS rc ON rc.url = base.url + ) AS r + LEFT JOIN ( + SELECT channel AS repoUrl, + 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' + GROUP BY channel + ) AS c ON c.repoUrl = r.url + LEFT JOIN ( + SELECT channel AS repoUrl, countIf(closedAt IS NULL) AS issuesOpenNow + FROM issues_analyzed + GROUP BY channel + ) AS n ON n.repoUrl = r.url + LEFT JOIN ( + SELECT channel AS repoUrl, + countIf(openedAt > now() - INTERVAL 6 MONTH) AS issuesOpenedLast6m, + countIf(openedAt <= now() - INTERVAL 6 MONTH AND openedAt > now() - INTERVAL 12 MONTH) AS issuesOpenedPrior6m, + countIf(openedAt > now() - INTERVAL 18 MONTH) AS issuesInWindow18m + FROM issues_analyzed + GROUP BY channel + ) AS w ON w.repoUrl = r.url + LEFT JOIN ( + SELECT channel AS repoUrl, countIf(openedAt > now() - INTERVAL 18 MONTH) AS prsInWindow18m + FROM pull_requests_analyzed + GROUP BY channel + ) AS p ON p.repoUrl = r.url + LEFT JOIN ( + SELECT repoUrl, countIf(status = 'OPEN' AND severity = 'CRITICAL') AS openCriticals + FROM vulnerabilities + GROUP BY repoUrl + ) AS v ON v.repoUrl = r.url + LEFT JOIN ( + SELECT r2.url AS repoUrl, max(rd.releaseDay) AS latestReleaseDay + FROM repos r2 + INNER JOIN packageRepos pr ON pr.repoId = r2.id + INNER JOIN ( + SELECT DISTINCT packageId, toDate(publishedAt) AS releaseDay + FROM versions WHERE publishedAt IS NOT NULL + ) rd ON rd.packageId = pr.packageId + GROUP BY r2.url + ) AS rl ON rl.repoUrl = r.url + +TYPE COPY +TARGET_DATASOURCE health_score_v2_lifecycle_ds +COPY_MODE replace +COPY_SCHEDULE 15 2 * * * diff --git a/services/libs/tinybird/pipes/health_score_v2_maintainer.pipe b/services/libs/tinybird/pipes/health_score_v2_maintainer.pipe new file mode 100644 index 0000000000..bd142b0630 --- /dev/null +++ b/services/libs/tinybird/pipes/health_score_v2_maintainer.pipe @@ -0,0 +1,119 @@ +DESCRIPTION > + Maintainer Health category (max 40 pts): bus factor (18), org diversity (7), maintainer + responsiveness (15). Graceful degradation (spec Layer 1 + 2): bus factor and org diversity are + `available` (real data found) or `blocked` (no matching rows at all, weight excluded from both + the numerator and rescale denominator) — a real score of 0 is distinct from blocked. + Responsiveness follows Joana's 2026-07-23 review (per-platform, not a blanket rule): + - GitHub/GitLab (and any other non-Gerrit host): scores 0/15 when there are no PRs or issues + in the window — that's real signal (unresponsive), not a data gap, so it's never `blocked`. + - Gerrit (review.opendev.org, gerrit.*): Gerrit has no issue tracker, so only PR/changeset + response time counts. Scores 0/15 when there's no changeset data; issue-response is excluded + from the calculation entirely rather than penalizing repos for a concept that doesn't exist there. + - Repos marked `excluded` in the repositories table (e.g. .github meta repos where no PRs or + issues are ever expected) are `blocked` for responsiveness regardless of platform — never + scored 0 for an absence that was never expected to be filled. + If available sub-signals cover <40% of the 40pt max, the category is `unavailable` (NULL), not + a fabricated 0. + - Split out of health_score_v2.pipe into its own copy pipe (2026-07-22) so this join runs + independently instead of being re-inlined into every other category's query plan — the combined + 6-node single-pipe version was taking 20-28+ minutes per run (vs ~13min pre-split) because + Tinybird inlines all upstream NODEs into one query per copy job, causing `repositories` and other + base tables to be re-scanned once per category. health_score_v2_results.pipe now reads this via + a cheap LEFT JOIN over the small materialized result instead. + - Bus factor fix (2026-07-24, IN-1196): `maintainers_roles_copy_ds.endDate` is a manually-curated + role record — it stays at the "still active" sentinel (1970-01-01) indefinitely unless someone + explicitly closes the role, so archived/dead repos (Xamarin.Forms, HPCToolkit, Ursa, SpaceVim + confirmed) retained a maxed-out busFactorScore=18 from maintainers with no commits/activity in + years. Bus factor now requires the maintainer to also have activityRelations activity on that + repo within the same trailing 12-month window used elsewhere in this pipe, not just an unexpired + role record. This was the direct cause of dead repos hitting healthScoreV2=100 after Layer 2 + rescaling (see LFX/cdp/health-score-v2-renormalization-bug.md). + +NODE health_score_v2_maintainer_calc +SQL > + SELECT + base.repoUrl AS repoUrl, + if(coveredWeight / 40.0 < 0.4, NULL, toNullable(toUInt8(round(least(40.0, rawScore * (40.0 / coveredWeight)))))) AS maintainerHealthScoreV2 + FROM ( + SELECT + repoUrl, + (busFactorScore + orgDiversityScore + responsivenessScore) AS rawScore, + (busFactorAvailable * 18 + orgDiversityAvailable * 7 + responsivenessAvailable * 15) AS coveredWeight + FROM ( + SELECT + allRepos.repoUrl AS repoUrl, + bf.repoUrl != '' AS busFactorAvailable, + multiIf(bf.busFactorCount >= 5, 18, bf.busFactorCount >= 3, 15, bf.busFactorCount = 2, 6, bf.busFactorCount = 1, 3, 0) AS busFactorScore, + od.repoUrl != '' AS orgDiversityAvailable, + multiIf(coalesce(od.orgCount, 0) >= 3, 7, coalesce(od.orgCount, 0) = 2, 5, coalesce(od.orgCount, 0) = 1, 2, 0) AS orgDiversityScore, + multiIf( + allRepos.isExcluded, false, + allRepos.isGerrit, r.repoUrl != '', + true + ) AS responsivenessAvailable, + multiIf( + allRepos.isGerrit, + -- Gerrit: PR/changeset response time only; issues don't exist here, never penalize their absence + multiIf( + r.repoUrl = '', 0, + r.medianPrResponseS < 86400, 15, + r.medianPrResponseS < 259200, 12, + r.medianPrResponseS < 604800, 9, + r.medianPrResponseS < 2592000, 5, + r.medianPrResponseS < 7776000, 2, + 0 + ), + -- GitHub/GitLab/other: no PR+issue data at all is a real 0 (unresponsive), not a data gap + multiIf( + r.repoUrl = '' AND ir.repoUrl = '', 0, + coalesce((r.medianPrResponseS + ir.medianIssueResponseS) / 2, r.medianPrResponseS, ir.medianIssueResponseS) < 86400, 15, + coalesce((r.medianPrResponseS + ir.medianIssueResponseS) / 2, r.medianPrResponseS, ir.medianIssueResponseS) < 259200, 12, + coalesce((r.medianPrResponseS + ir.medianIssueResponseS) / 2, r.medianPrResponseS, ir.medianIssueResponseS) < 604800, 9, + coalesce((r.medianPrResponseS + ir.medianIssueResponseS) / 2, r.medianPrResponseS, ir.medianIssueResponseS) < 2592000, 5, + coalesce((r.medianPrResponseS + ir.medianIssueResponseS) / 2, r.medianPrResponseS, ir.medianIssueResponseS) < 7776000, 2, + 0 + ) + ) AS responsivenessScore + FROM ( + 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 + ) AS allRepos + LEFT JOIN ( + 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 + ) AS bf ON bf.repoUrl = allRepos.repoUrl + LEFT JOIN ( + SELECT channel AS repoUrl, count(DISTINCT organizationId) AS orgCount + FROM activityRelations + WHERE timestamp > now() - INTERVAL 12 MONTH AND organizationId != '' + GROUP BY channel + ) AS od ON od.repoUrl = allRepos.repoUrl + LEFT JOIN ( + SELECT channel AS repoUrl, quantile(0.5)(reviewedInSeconds) AS medianPrResponseS + FROM pull_requests_analyzed + WHERE openedAt > now() - INTERVAL 12 MONTH AND reviewedInSeconds IS NOT NULL + GROUP BY channel + ) AS r ON r.repoUrl = allRepos.repoUrl + LEFT JOIN ( + SELECT channel AS repoUrl, quantile(0.5)(respondedInSeconds) AS medianIssueResponseS + FROM issues_analyzed + WHERE openedAt > now() - INTERVAL 12 MONTH AND respondedInSeconds IS NOT NULL + GROUP BY channel + ) AS ir ON ir.repoUrl = allRepos.repoUrl + ) + ) AS base + +TYPE COPY +TARGET_DATASOURCE health_score_v2_maintainer_ds +COPY_MODE replace +COPY_SCHEDULE 0 2 * * * diff --git a/services/libs/tinybird/pipes/health_score_v2_security.pipe b/services/libs/tinybird/pipes/health_score_v2_security.pipe new file mode 100644 index 0000000000..7798bf3572 --- /dev/null +++ b/services/libs/tinybird/pipes/health_score_v2_security.pipe @@ -0,0 +1,85 @@ +DESCRIPTION > + Security & Supply Chain category (max 35 pts): open vulnerabilities (10), OpenSSF Scorecard (7, + blocked for repos with no repos-table row — effectively non-GitHub hosts), security practices + (spec max 8, but capped here at 7 achievable — the spec's 6th bullet, security_contact_email, + has no corresponding column anywhere in our GitHub enrichment data, so it's permanently omitted + like Supply Chain Integrity below; blocked entirely when repos-table row missing), dependency + health (5, blocked when the repo has no published packages so vulnerability exposure is unknown), + supply chain integrity (5, permanently blocked — hardcoded per spec, provenance/2FA data not yet + in the pipeline). Graceful degradation (spec Layer 1+2): category_subtotal = + SUM(available_sub_scores) * (35 / SUM(available_sub_max_weights)); category is `unavailable` + (NULL) when covered weight is <40% of 35. + - Split out of health_score_v2.pipe into its own copy pipe (2026-07-22) — see + health_score_v2_maintainer.pipe description for why. + +NODE health_score_v2_security_calc +SQL > + SELECT + repoUrl, + if(coveredWeight / 35.0 < 0.4, NULL, toNullable(toUInt8(round(least(35.0, rawScore * (35.0 / coveredWeight)))))) AS securitySupplyChainScoreV2 + FROM ( + SELECT + repoUrl, + (openVulnScore + scorecardAvailable * scorecardScorePts + securityPracticesAvailable * securityPracticesScore + + dependencyHealthAvailable * dependencyHealthScore) AS rawScore, + (10 + scorecardAvailable * 7 + securityPracticesAvailable * 7 + dependencyHealthAvailable * 5) AS coveredWeight + FROM ( + SELECT + allRepos.url AS repoUrl, + greatest(0, 10 - 6*coalesce(vc.openCriticals,0) - 3*coalesce(vc.openHighs,0) - 1*coalesce(vc.openModerates,0)) AS openVulnScore, + toUInt8(round(least(toFloat64OrZero(rd.scorecardScore), 10) * 0.7)) AS scorecardScorePts, + (rd.url != '') AS scorecardAvailable, + (2*coalesce(rd.securityPolicyEnabled,0) + 2*coalesce(rd.branchProtectionEnabled,0) + + if(coalesce(rd.branchProtectionRequiredReviews,0) >= 1, 1, 0) + + coalesce(rd.branchProtectionRequiresStatusChecks,0) + + if(coalesce(rd.branchProtectionAllowsForcePush,1) = 0, 1, 0)) AS securityPracticesScore, + (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 + LEFT JOIN ( + SELECT url, + argMax(scorecardScore, updatedAt) AS scorecardScore, + argMax(securityPolicyEnabled, updatedAt) AS securityPolicyEnabled, + argMax(branchProtectionEnabled, updatedAt) AS branchProtectionEnabled, + argMax(branchProtectionRequiredReviews, updatedAt) AS branchProtectionRequiredReviews, + argMax(branchProtectionRequiresStatusChecks, updatedAt) AS branchProtectionRequiresStatusChecks, + argMax(branchProtectionAllowsForcePush, updatedAt) AS branchProtectionAllowsForcePush + FROM repos + GROUP BY url + ) AS rd ON rd.url = allRepos.url + LEFT JOIN ( + SELECT repoUrl, + countIf(status = 'OPEN' AND severity = 'CRITICAL') AS openCriticals, + countIf(status = 'OPEN' AND severity = 'HIGH') AS openHighs, + countIf(status = 'OPEN' AND severity = 'MODERATE') AS openModerates + FROM vulnerabilities + GROUP BY repoUrl + ) AS vc ON vc.repoUrl = allRepos.url + LEFT JOIN ( + SELECT r.url AS repoUrl, count(DISTINCT pd.dependsOnId) AS vulnerableDeps + FROM repos r + INNER JOIN packageRepos pr ON pr.repoId = r.id + INNER JOIN packageDependencies pd ON pd.packageId = pr.packageId + WHERE pd.dependsOnId IN ( + SELECT ap.packageId FROM advisoryPackages ap + INNER JOIN advisories a ON a.id = ap.advisoryId + WHERE a.severity IN ('HIGH', 'CRITICAL') + ) + GROUP BY r.url + ) AS dh ON dh.repoUrl = allRepos.url + LEFT JOIN ( + -- whether the repo has ANY published packages at all — determines dependency-health + -- availability (0 vulnerable deps out of 0 tracked deps is unknown, not "healthy") + SELECT r.url AS repoUrl, count() AS pkgCount + FROM repos r + INNER JOIN packageRepos pr ON pr.repoId = r.id + GROUP BY r.url + ) AS pkgs ON pkgs.repoUrl = allRepos.url + ) + ) + +TYPE COPY +TARGET_DATASOURCE health_score_v2_security_ds +COPY_MODE replace +COPY_SCHEDULE 5 2 * * * diff --git a/services/libs/tinybird/pipes/project_insights.pipe b/services/libs/tinybird/pipes/project_insights.pipe index 5acfa51530..cc98c50ea1 100644 --- a/services/libs/tinybird/pipes/project_insights.pipe +++ b/services/libs/tinybird/pipes/project_insights.pipe @@ -12,6 +12,7 @@ DESCRIPTION > - `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. TAGS "Insights, Widget", "Project" @@ -46,7 +47,12 @@ SQL > starsPrevious365Days, forksPrevious365Days, activeContributorsPrevious365Days, - activeOrganizationsPrevious365Days + activeOrganizationsPrevious365Days, + healthScoreV2, + healthLabel, + lifecycleLabel, + impactScore, + impactLabel FROM project_insights_copy_ds WHERE type = 'project' diff --git a/services/libs/tinybird/pipes/project_insights_copy.pipe b/services/libs/tinybird/pipes/project_insights_copy.pipe index c5d8708202..6c373ece72 100644 --- a/services/libs/tinybird/pipes/project_insights_copy.pipe +++ b/services/libs/tinybird/pipes/project_insights_copy.pipe @@ -85,6 +85,54 @@ SQL > WHERE timestamp >= now() - INTERVAL 730 DAY AND timestamp <= now() GROUP BY segmentId +NODE project_insights_copy_health_v2_project +DESCRIPTION > + Lifecycle, Health Score v2, and Impact Score all rolled up to project level from the pre-computed + per-repo values in health_score_v2_repo_copy_ds (materialized by health_score_v2.pipe — the + expensive per-signal joins run there as their own copy job, not inline here). Per spec section 5/6: + - healthScoreV2: straight mean across the project's enabled, non-excluded repos. + - lifecycleLabelV2: best-state-wins (active > stable > declining > abandoned > archived), with + NULL (no usable signal, see health_score_v2_lifecycle.pipe) as the most-cautious outcome. + `groupArray` silently drops NULL entries, so a project with at least one repo in a real state + already rolls up correctly for free — NULL only surfaces at the project level when EVERY repo + contributes NULL, or none contribute at all. + Guarded explicitly (2026-07-24, IN-1196) because `groupArray` over an empty or all-NULL input + returns an EMPTY array, and `arrayElement([], 1)` on that empty array returns an empty string + `''`, not NULL — this exact mechanism was confirmed live as the root cause of 5 blank- + lifecycleLabel projects found during IN-1196 verification (Charliecloud, GNU Binutils and GDB, + GravityView, GridGain Community Edition, Hitomi-Downloader): each has its sole repo marked + `repositories.excluded = true` (confirmed on GravityView specifically), so the `WHERE + rep.excluded = false` filter below drops every repo for that project, `groupArray` sees zero + rows, and the old unguarded `arrayElement` returned `''`. `if(empty(...), NULL, ...)` makes + that case (and the all-NULL-lifecycle case) resolve to a real NULL instead. The 5 named + projects will still show a blank/NULL lifecycle after this fix — this only stops it from being + a silent empty string; the underlying "this project's only repo is excluded" data situation is + unrelated to IN-1196 and not addressed here. + - impactScoreRaw: MAX across the project's repos (each repo's own value is already a MAX over its + published packages) — matches the spec's "MAX(packages.impact) over packages published by any + repo in the project," since max-of-maxes = max-over-all. + +SQL > + SELECT + rep.insightsProjectId AS insightsProjectId, + avg(hv2.healthScoreV2) AS healthScoreV2, + if( + empty(groupArray(hv2.lifecycleLabelV2)), + NULL, + toNullable(arrayElement( + arraySort( + x -> indexOf(['active', 'stable', 'declining', 'abandoned', 'archived'], x), + groupArray(hv2.lifecycleLabelV2) + ), + 1 + )) + ) AS lifecycleLabelV2, + maxOrNull(hv2.impactScore) AS impactScoreRaw + FROM repositories rep FINAL + INNER JOIN health_score_v2_repo_copy_ds hv2 ON hv2.repoUrl = rep.url + WHERE rep.insightsProjectId != '' AND rep.enabled = true AND rep.excluded = false + GROUP BY rep.insightsProjectId + NODE project_insights_copy_project_results DESCRIPTION > Join all project insights together @@ -120,11 +168,39 @@ SQL > pm.starsPrevious365Days AS starsPrevious365Days, pm.forksPrevious365Days AS forksPrevious365Days, pm.activeContributorsPrevious365Days AS activeContributorsPrevious365Days, - pm.activeOrganizationsPrevious365Days AS activeOrganizationsPrevious365Days + pm.activeOrganizationsPrevious365Days AS activeOrganizationsPrevious365Days, + toUInt8(round(hv2.healthScoreV2)) AS healthScoreV2, + multiIf( + hv2.healthScoreV2 IS NULL, + NULL, + hv2.healthScoreV2 >= 85, + 'excellent', + hv2.healthScoreV2 >= 70, + 'healthy', + hv2.healthScoreV2 >= 50, + 'fair', + hv2.healthScoreV2 >= 30, + 'concerning', + 'critical' + ) AS healthLabel, + hv2.lifecycleLabelV2 AS lifecycleLabel, + hv2.impactScoreRaw AS impactScore, + multiIf( + hv2.impactScoreRaw IS NULL, + NULL, + hv2.impactScoreRaw >= 85, + 'foundational', + hv2.impactScoreRaw >= 60, + 'major', + hv2.impactScoreRaw >= 30, + 'moderate', + 'minor' + ) AS impactLabel FROM project_insights_copy_base_projects AS base LEFT JOIN project_insights_copy_dependency_metrics AS dep ON base.slug = dep.slug LEFT JOIN project_insights_copy_achievements AS ach ON base.slug = ach.slug LEFT JOIN project_insights_copy_period_metrics AS pm USING (segmentId) + LEFT JOIN project_insights_copy_health_v2_project AS hv2 ON base.id = hv2.insightsProjectId NODE project_insights_copy_repo_base DESCRIPTION > @@ -209,10 +285,38 @@ SQL > COALESCE(rm.starsPrevious365Days, 0) AS starsPrevious365Days, COALESCE(rm.forksPrevious365Days, 0) AS forksPrevious365Days, COALESCE(rm.activeContributorsPrevious365Days, 0) AS activeContributorsPrevious365Days, - COALESCE(rm.activeOrganizationsPrevious365Days, 0) AS activeOrganizationsPrevious365Days + COALESCE(rm.activeOrganizationsPrevious365Days, 0) AS activeOrganizationsPrevious365Days, + toUInt8(round(hv2.healthScoreV2)) AS healthScoreV2, + multiIf( + hv2.healthScoreV2 IS NULL, + NULL, + hv2.healthScoreV2 >= 85, + 'excellent', + hv2.healthScoreV2 >= 70, + 'healthy', + hv2.healthScoreV2 >= 50, + 'fair', + hv2.healthScoreV2 >= 30, + 'concerning', + 'critical' + ) AS healthLabel, + hv2.lifecycleLabelV2 AS lifecycleLabel, + hv2.impactScore AS impactScore, + multiIf( + hv2.impactScore IS NULL, + NULL, + hv2.impactScore >= 85, + 'foundational', + hv2.impactScore >= 60, + 'major', + hv2.impactScore >= 30, + 'moderate', + 'minor' + ) AS impactLabel FROM project_insights_copy_repo_base AS base LEFT JOIN repo_health_score_copy_ds AS hs ON base.repoUrl = hs.channel LEFT JOIN project_insights_copy_repo_period_metrics AS rm ON base.repoUrl = rm.channel + LEFT JOIN health_score_v2_repo_copy_ds AS hv2 ON base.repoUrl = hv2.repoUrl NODE project_insights_copy_results DESCRIPTION > diff --git a/services/libs/tinybird/pipes/project_repo_insights.pipe b/services/libs/tinybird/pipes/project_repo_insights.pipe index 1ea2553e0c..6553383db5 100644 --- a/services/libs/tinybird/pipes/project_repo_insights.pipe +++ b/services/libs/tinybird/pipes/project_repo_insights.pipe @@ -11,6 +11,7 @@ DESCRIPTION > - `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. TAGS "Insights, Widget", "Project", "Repository" @@ -47,7 +48,12 @@ SQL > starsPrevious365Days, forksPrevious365Days, activeContributorsPrevious365Days, - activeOrganizationsPrevious365Days + activeOrganizationsPrevious365Days, + healthScoreV2, + healthLabel, + lifecycleLabel, + impactScore, + impactLabel FROM project_insights_copy_ds WHERE 1 = 1