Skip to content

Instantly share code, notes, and snippets.

@soderlind
Last active July 21, 2026 07:44
Show Gist options
  • Select an option

  • Save soderlind/3eb07662253264d91acb71834afda4d9 to your computer and use it in GitHub Desktop.

Select an option

Save soderlind/3eb07662253264d91acb71834afda4d9 to your computer and use it in GitHub Desktop.
Adjusted score = 0.70 B + 0.20 ( 5 W ) + 0.10 ( 5 M )

Bayesian rating with compatibility and maintenance adjustments

The adjusted extension score ranks WordPress themes and plugins using three signals:

$$ \text{Adjusted score} = 0.70B + 0.20(5W) + 0.10(5M) $$

Where:

  • $B$ = Bayesian user rating, from 0–5
  • $W$ = compatibility confidence, from 0–1
  • $M$ = maintenance confidence, from 0–1

Recommended weighting:

  • 70% user ratings
  • 20% verified compatibility
  • 10% maintenance

The original star rating should remain visible. The adjusted score is intended for ranking, not as a replacement for the rating users submitted.

1. Bayesian rating

The Bayesian rating prevents extensions with only a few ratings from ranking unrealistically high.

$$ B = \frac{vR + mC}{v + m} $$

Where:

  • $R$ = extension's average rating
  • $v$ = number of ratings
  • $C$ = global average across all extensions
  • $m$ = confidence threshold

An extension with two perfect ratings is pulled toward the global average more strongly than one with hundreds of ratings.

2. Compatibility confidence

Do not treat the extension version or Tested up to as proof of compatibility. Both are controlled by the author.

Separate compatibility into:

  • Claimed compatibility: metadata covers the target WordPress version.
  • Verified compatibility: independent tests pass.

$$ W = 0.20C_c + 0.80V $$

Where:

  • $C_c$ = claimed compatibility, from 0–1
  • $V$ = verified compatibility, from 0–1

The subscript in $C_c$ distinguishes claimed compatibility from the global rating average $C$ used in the Bayesian formula.

Changing Tested up to alone can therefore provide at most 20% compatibility confidence.

Verification can include:

  • Installation
  • Activation
  • Front-end request
  • Admin request
  • PHP error detection
  • Automated tests
  • Plugin Check or Theme Check
  • Theme template and block-editor checks

Repeated metadata-only bumps can reduce confidence when no verification exists.

3. Maintenance confidence

Use the number of days since the last substantive or independently verified update, not the repository's raw “last updated” timestamp.

$$ M = 2^{-\frac{a}{H}} $$

Where:

  • $a$ = age in days since the last substantive or verified update
  • $H$ = maintenance half-life in days

With a two-year half-life:

  • Today: 1.00
  • One year: approximately 0.71
  • Two years: 0.50
  • Four years: 0.25

This prevents an author from resetting maintenance age by changing only a version number or Tested up to.

Example: verified extension

This input works for either a theme or plugin:

$result = calculate_adjusted_rating(
	array(
		'rating'                     => 4.7,
		'rating_count'               => 120,
		'global_average'             => 4.2,
		'confidence_threshold'       => 20,

		'claims_target_version'      => true,
		'activation_passed'          => true,
		'runtime_tests_passed'       => true,
		'automated_tests_passed'     => false,

		'metadata_only_release'      => true,
		'consecutive_bumps'          => 3,

		'days_since_verified_update' => 500,
		'maintenance_half_life'      => 730,
	)
);

print_r( $result );

Result:

Array
(
    [adjusted_rating] => 4.31
    [bayesian_rating] => 4.63
    [compatibility_confidence] => 0.76
    [maintenance_confidence] => 0.622
)

Although the latest release changed only metadata, installation and runtime testing independently verified compatibility.

Example: unverified metadata bump

$result = calculate_adjusted_rating(
	array(
		'rating'                     => 4.7,
		'rating_count'               => 120,
		'global_average'             => 4.2,
		'confidence_threshold'       => 20,

		'claims_target_version'      => true,
		'activation_passed'          => false,
		'runtime_tests_passed'       => false,
		'automated_tests_passed'     => false,

		'metadata_only_release'      => true,
		'consecutive_bumps'          => 3,

		'days_since_verified_update' => 500,
	)
);

print_r( $result );

Result:

Array
(
    [adjusted_rating] => 3.70
    [bayesian_rating] => 4.63
    [compatibility_confidence] => 0.15
    [maintenance_confidence] => 0.622
)

The original user rating remains strong, but the ranking drops because the compatibility claim has no supporting evidence.

Detecting metadata-only releases

Common metadata files include:

readme.txt
README.md
CHANGELOG.md
changelog.txt

Normalize version-only fields before comparing releases:

Version
Stable tag
Tested up to
Requires at least
Requires PHP

Functional files can include:

*.php
*.js
*.css
*.scss
*.json
*.html
templates/*
parts/*
patterns/*
blocks/*
assets/*

For themes, style.css contains both metadata and functional CSS. Strip or normalize only its theme header before calculating the content hash. Actual stylesheet changes must still count as substantive maintenance.

Interpretation

The adjusted score should be described as an adjusted extension score, not a user rating. It rewards established ratings, independently demonstrated compatibility, and sustained maintenance while making metadata-only manipulation much less valuable.

<?php
/**
* Calculate a Bayesian rating.
*
* @param float $rating Extension's average user rating.
* @param int $rating_count Number of user ratings.
* @param float $global_average Average across all extensions.
* @param int $confidence Minimum ratings for strong confidence.
* @return float Rating between 0.0 and 5.0.
*/
function calculate_bayesian_rating(
float $rating,
int $rating_count,
float $global_average,
int $confidence
): float {
$rating = max( 0.0, min( 5.0, $rating ) );
$rating_count = max( 0, $rating_count );
$global_average = max( 0.0, min( 5.0, $global_average ) );
$confidence = max( 1, $confidence );
return (
( $rating_count * $rating ) +
( $confidence * $global_average )
) / ( $rating_count + $confidence );
}
/**
* Calculate compatibility confidence.
*
* Works for WordPress themes and plugins.
*
* @param bool $claims_target_version Metadata covers the target version.
* @param bool $activation_passed Installation and activation passed.
* @param bool $runtime_tests_passed Front-end and admin checks passed.
* @param bool $automated_tests_passed Automated tests passed.
* @param bool $metadata_only_release Functional files were unchanged.
* @param int $consecutive_bumps Consecutive metadata-only bumps.
* @return float Confidence between 0.0 and 1.0.
*/
function calculate_compatibility_confidence(
bool $claims_target_version,
bool $activation_passed,
bool $runtime_tests_passed,
bool $automated_tests_passed,
bool $metadata_only_release,
int $consecutive_bumps
): float {
$consecutive_bumps = max( 0, $consecutive_bumps );
// Self-reported metadata contributes at most 20%.
$claimed = $claims_target_version ? 1.0 : 0.0;
// Independent verification contributes up to 80%.
$verified = 0.0;
if ( $activation_passed ) {
$verified += 0.30;
}
if ( $runtime_tests_passed ) {
$verified += 0.40;
}
if ( $automated_tests_passed ) {
$verified += 0.30;
}
$confidence = ( 0.20 * $claimed ) + ( 0.80 * $verified );
/*
* Repeated metadata bumps reduce confidence unless there is
* sufficient independent evidence of compatibility.
*/
if (
$metadata_only_release &&
$consecutive_bumps >= 2 &&
$verified < 0.70
) {
$confidence *= 0.75;
}
return max( 0.0, min( 1.0, $confidence ) );
}
/**
* Calculate maintenance confidence using exponential decay.
*
* Age should be measured from the most recent substantive or
* independently verified update—not a raw metadata timestamp.
*
* @param int $days_since_update Days since verified/substantive update.
* @param int $half_life_days Number of days until confidence halves.
* @return float Confidence between 0.0 and 1.0.
*/
function calculate_maintenance_confidence(
int $days_since_update,
int $half_life_days = 730
): float {
$days_since_update = max( 0, $days_since_update );
$half_life_days = max( 1, $half_life_days );
return pow(
0.5,
$days_since_update / $half_life_days
);
}
/**
* Calculate the complete adjusted extension rating.
*
* @param array<string, mixed> $data Rating and verification data.
* @return array<string, float> Score and its individual components.
*/
function calculate_adjusted_rating( array $data ): array {
$bayesian = calculate_bayesian_rating(
(float) ( $data['rating'] ?? 0.0 ),
(int) ( $data['rating_count'] ?? 0 ),
(float) ( $data['global_average'] ?? 0.0 ),
(int) ( $data['confidence_threshold'] ?? 20 )
);
$compatibility = calculate_compatibility_confidence(
(bool) ( $data['claims_target_version'] ?? false ),
(bool) ( $data['activation_passed'] ?? false ),
(bool) ( $data['runtime_tests_passed'] ?? false ),
(bool) ( $data['automated_tests_passed'] ?? false ),
(bool) ( $data['metadata_only_release'] ?? false ),
(int) ( $data['consecutive_bumps'] ?? 0 )
);
$maintenance = calculate_maintenance_confidence(
(int) ( $data['days_since_verified_update'] ?? 0 ),
(int) ( $data['maintenance_half_life'] ?? 730 )
);
$adjusted = (
0.70 * $bayesian +
0.20 * ( 5.0 * $compatibility ) +
0.10 * ( 5.0 * $maintenance )
);
return array(
'adjusted_rating' => round( $adjusted, 2 ),
'bayesian_rating' => round( $bayesian, 2 ),
'compatibility_confidence' => round( $compatibility, 3 ),
'maintenance_confidence' => round( $maintenance, 3 ),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment