diff --git a/web/scripts/__tests__/check-governance-health.test.ts b/web/scripts/__tests__/check-governance-health.test.ts index 09cbaf55..64b0c0a0 100644 --- a/web/scripts/__tests__/check-governance-health.test.ts +++ b/web/scripts/__tests__/check-governance-health.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import { computeGini, percentile } from '../../shared/governance-snapshot'; import type { ActivityData, Comment, @@ -9,7 +10,6 @@ import { buildHealthReport, computeCrossRoleReviewRate, computeDataWindowDays, - computeGini, computeMergeBacklogDepth, computeMergeLatency, computeContestedRate, @@ -20,7 +20,6 @@ import { extractRole, hadQuorumFailure, inferEligibleVoterCount, - percentile, resolveActivityFile, } from '../check-governance-health'; diff --git a/web/scripts/check-governance-health.ts b/web/scripts/check-governance-health.ts index 91a32932..646cb4f0 100644 --- a/web/scripts/check-governance-health.ts +++ b/web/scripts/check-governance-health.ts @@ -21,6 +21,7 @@ import { existsSync, readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { computeGini, percentile } from '../shared/governance-snapshot'; import type { ActivityData, Comment, @@ -156,33 +157,6 @@ export function extractRole(login: string): string | null { return match ? match[1] : null; } -/** - * Compute the p-th percentile of a pre-sorted ascending array. - * Returns null for empty arrays. - */ -export function percentile(sorted: number[], p: number): number | null { - if (sorted.length === 0) return null; - const index = Math.ceil((p / 100) * sorted.length) - 1; - return sorted[Math.max(0, index)]; -} - -/** - * Compute the Gini coefficient for an array of non-negative values. - * Returns 0 for arrays of length ≤ 1 or all-zero arrays. - */ -export function computeGini(values: number[]): number { - if (values.length <= 1) return 0; - const sorted = [...values].sort((a, b) => a - b); - const n = sorted.length; - const total = sorted.reduce((a, b) => a + b, 0); - if (total === 0) return 0; - let sumOfDiffs = 0; - for (let i = 0; i < n; i++) { - sumOfDiffs += (2 * (i + 1) - n - 1) * sorted[i]; - } - return sumOfDiffs / (n * total); -} - // ────────────────────────────────────────────── // Metric computation // ────────────────────────────────────────────── diff --git a/web/shared/governance-snapshot.ts b/web/shared/governance-snapshot.ts index 97e06ae8..75597a7f 100644 --- a/web/shared/governance-snapshot.ts +++ b/web/shared/governance-snapshot.ts @@ -447,6 +447,16 @@ export function computeGini(values: number[]): number { return sumOfDiffs / (n * total); } +/** + * Compute the p-th percentile of a pre-sorted ascending array. + * Returns null for empty arrays. + */ +export function percentile(sorted: number[], p: number): number | null { + if (sorted.length === 0) return null; + const index = Math.ceil((p / 100) * sorted.length) - 1; + return sorted[Math.max(0, index)]; +} + /** * Proposals resolved per day over the trailing 7 days. * "Resolved" = implemented, rejected, or inconclusive.