From 7adf944e4c79d570883fd7f8437c70870df3b959 Mon Sep 17 00:00:00 2001 From: Phanuella Date: Thu, 22 Oct 2026 09:19:37 +0100 Subject: [PATCH 1/4] feat: delegate accountability score in directory and voter-history views (#685) --- docs/delegate-accountability.md | 36 ++++++++++ src/app/governance/delegates/page.tsx | 6 +- src/app/governance/voter/[address]/page.tsx | 9 ++- src/components/AccountabilityBadge.tsx | 23 +++++++ src/lib/delegateAccountability.test.ts | 66 ++++++++++++++++++ src/lib/delegateAccountability.ts | 75 +++++++++++++++++++++ 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 docs/delegate-accountability.md create mode 100644 src/components/AccountabilityBadge.tsx create mode 100644 src/lib/delegateAccountability.test.ts create mode 100644 src/lib/delegateAccountability.ts diff --git a/docs/delegate-accountability.md b/docs/delegate-accountability.md new file mode 100644 index 00000000..1ff3bea6 --- /dev/null +++ b/docs/delegate-accountability.md @@ -0,0 +1,36 @@ +# Delegate accountability score + +The accountability score is a single 0-100 signal shown as a badge in the delegate directory +(`/governance/delegates`) and the voter-history page header (`/governance/voter/[address]`). It sits +alongside the raw `participationRate` and `proposalsVoted` stats and does not replace them. It is +informational only and must never gate an on-chain action. + +Implementation: `src/lib/delegateAccountability.ts` (`computeAccountabilityScore`), a pure function +with no fetching. + +## Formula + +Three components, each in the range 0-1: + +| Component | Weight | Definition | +| -------------- | ------ | ---------- | +| Participation | 0.5 | The delegate's `participationRate`. | +| Activity | 0.3 | Mean recency weight of the delegate's votes, where a vote of age `d` days weighs `0.5^(d / 90)` (90-day half-life). | +| Alignment | 0.2 | Share of votes (with a known outcome) that match the winning choice. | + +`score = round(100 * sum(weight_i * value_i) / sum(weight_i))` + +Components that cannot be computed are dropped and the remaining weights are renormalised: + +- No per-proposal vote history: only participation is used (the directory currently has no history). +- No votes with a known majority outcome: alignment is skipped. + +## Insufficient history + +If `max(vote history length, proposalsVoted) < 3`, no number is shown. The badge reads +"Insufficient history" so new delegates are neither flattered nor penalised. + +## Limitations + +Majority alignment is a descriptive signal, not a judgement: voting with a minority is legitimate. +It carries the lowest weight for that reason. diff --git a/src/app/governance/delegates/page.tsx b/src/app/governance/delegates/page.tsx index 76a1db7c..18962756 100644 --- a/src/app/governance/delegates/page.tsx +++ b/src/app/governance/delegates/page.tsx @@ -6,6 +6,7 @@ import { useDelegation } from '@/hooks/useDelegation'; import { sanitizeText } from '@/lib/textSafety'; import { isSameStellarAddress, isValidStellarAddress } from '@/lib/stellarAddress'; import { secureLogger } from '@/lib/secureLogging'; +import { AccountabilityBadge } from '@/components/AccountabilityBadge'; interface DelegateProfile { address: string; @@ -231,7 +232,10 @@ export default function DelegatesPage() {
  • -

    {sanitizeText(d.name)}

    +

    + {sanitizeText(d.name)} + +

    {d.address}