{"methodology_version":"v4.1","pipeline":{"measurement":{"description":"The plugin measures the same page multiple times: up to 5 baseline passes (all plugins active) then up to 3 passes per component (with the component disabled through an MU-loader). Each pass captures 7 metrics: duration_ms, cpu_ms (via getrusage), memory_peak, sql_queries, scripts_count, styles_count, external_calls. The plugin can stop early once the duration sample is tight (max\u2212min \u2264 max(50ms, 10% of the median)), with floors of 3 baseline \/ 2 component passes \u2014 this preserves precision while shortening total scan time on stable hosts.","baseline_passes_per_scan":{"min":3,"max":5,"note":"Plugin runs up to max; stops at min if the sample is already tight."},"component_passes_per_scan":{"min":2,"max":3,"note":"Plugin runs up to max; stops at min if the sample is already tight."},"bare_passes_per_scan":{"description":"Synthetic host-floor passes (methodology v3.2). The MU-loader disables every plugin, switches to the default theme and short-circuits on `init` priority 1, emitting a minimal body plus the metrics marker. Measures WP+server boot time independent of the scanned URL. Currently stored in `raw_payload` and surfaced as `bare_*` scalars for future host-normalized scoring; not yet consumed by the public score.","count":"1..7 passes (optional, skipped silently when the plugin or scan cannot collect them)"}},"aggregation":{"description":"Server-side, each pass set is filtered (Tukey k=1.5 on the IQR), then aggregated by median. The half-width of the 95% confidence interval is computed using Student's t(0.975, n-1) \u2014 not a Gaussian approximation, which would underestimate uncertainty for small samples.","outlier_filter":{"method":"Tukey","k":1.5,"min_n_to_apply":4},"point_estimate":"median","confidence_interval":"Student t (95%, two-sided)"},"isolation":{"description":"A component is measured by replaying the page with that component disabled. Two safeguards prevent the disable from fataling the request when its public API is called by the theme or other plugins.","compatibility_shims":{"description":"For popular plugins whose public API is consumed externally (functions like get_field, pll__, tribe_get_*), the MU-loader installs no-op stubs during the per-component pass. The plugin is removed from option_active_plugins; its functions still resolve to safe defaults so the page renders without fatal. Function-level only \u2014 fake objects\/classes are deliberately not provided.","covered_slugs":["advanced-custom-fields","advanced-custom-fields-pro","meta-box","meta-box-aio","pods","polylang","polylang-pro","sitepress-multilingual-cms","wordpress-seo","wordpress-seo-premium","seo-by-rank-math","seo-by-rank-math-pro","the-events-calendar","gravityforms"]},"non_isolable_detection":{"description":"When the first pass tripped a deterministic fatal (undefined function, class not found), the runner stops retrying and marks the component non_isolable. These components are visible in the local report as \"could not be safely measured\" but are NOT sent as scoring rows \u2014 the public ranking only contains validated measurements. Affected slugs are sent separately under non_isolable_slugs and archived for shim prioritisation, never aggregated.","retry_policy":"short-circuit after the first deterministic fatal (no second attempt)"}},"differentiation":{"description":"A component's own impact is: delta = median(baseline) \u2212 median(baseline without the component). Negative deltas (measurement noise) are clamped to 0.","cluster_mode":{"description":"When a \"core\" plugin (typically WooCommerce) cannot be safely disabled in isolation because it would fatal its dependents, the plugin disables the whole cluster at once and reports the dependents as `cluster_slugs`. By default the resulting delta would be the cost of \"core + dependents\", which is unfair to the core. Methodology v3.1 introduced the `core_alone_ref_passes` extra batch \u2014 passes taken with dependents disabled but the core kept active \u2014 letting Pulse derive a fairer delta:","formula":"core_alone_delta = aggregate(core_alone_ref_passes) \u2212 aggregate(passes)","single_dependent_optimization":"When the cluster has exactly one dependent (since plugin v0.19.0), no dedicated reference batch is taken: the dependent's own component pass already measures \"core alive, dependent disabled\" \u2014 the same condition. The plugin reuses those passes verbatim as `core_alone_ref_passes`. No precision impact; just one fewer round-trip.","fallback":"When `core_alone_ref_passes` are missing (legacy clusters from older plugin versions, or 2+ dependents with no reference batch collected), the scan is flagged `incomplete = true` and excluded from the public ranking. The cluster-level delta is still surfaced in the response under `cluster_delta_*` for the plugin's own UI."}},"scoring":{"description":"Score 0-100, weighted sum of 5 sub-scores. For each metric we apply a linear ramp between a low threshold (0 pt) and a high threshold (max weight). CPU and memory are normalized as a RATIO of the baseline so they stay invariant across machines; SQL\/assets\/HTTP remain absolute because they are already invariant by construction.","metrics":[{"key":"cpu_ratio","kind":"ratio","unit":"fraction of baseline CPU (0..1)","low":0.02,"high":0.3,"weight_max":30,"ramp":"linear (clamped 0..weight_max)"},{"key":"memory_ratio","kind":"ratio","unit":"fraction of baseline peak memory (0..1)","low":0.02,"high":0.4,"weight_max":20,"ramp":"linear (clamped 0..weight_max)"},{"key":"sql","kind":"absolute","unit":"extra SQL queries","low":2,"high":60,"weight_max":20,"ramp":"linear (clamped 0..weight_max)"},{"key":"assets","kind":"absolute","unit":"extra scripts + styles enqueued","low":1,"high":15,"weight_max":15,"ramp":"linear (clamped 0..weight_max)"},{"key":"external_http","kind":"absolute","unit":"extra external HTTP calls","low":0,"high":5,"weight_max":15,"ramp":"linear (clamped 0..weight_max)"}]},"grading":{"description":"The score is mapped to an A-E letter using absolute (not relative) bounds. A better ecosystem = more A-grades.","thresholds":[{"grade":"A","score_min":0,"score_max":85},{"grade":"B","score_min":86,"score_max":65},{"grade":"C","score_min":66,"score_max":45},{"grade":"D","score_min":46,"score_max":25},{"grade":"E","score_min":26,"score_max":100}]},"guards":{"description":"Guards applied before aggregating a scan into a version's public median. A rejected scan is still stored for audit.","baseline_too_light":{"rule":"baseline_cpu_ms < CPU_MS_FLOOR","value":50,"unit":"milliseconds","why":"On a ridiculously thin baseline, ratios become extreme and noisy."},"baseline_unhealthy":{"rule":"baseline_cpu_ms > UNHEALTHY_MULTIPLIER \u00d7 global_median_cpu_ms (same page_type)","multiplier":3,"why":"On a bloated site (50 plugins already active), a heavy plugin looks negligible."},"plausibility":{"description":"Inter-metric heuristics (see PlausibilityChecker). Score 0-100; below the reject threshold the scan is excluded.","reject_below":40,"downweight_below":70}},"publication":{"description":"The public score on a plugin or theme page is consolidated over a sliding window (see `consolidation` below). The per-version medians (used for the version-by-version history table) are published when backed by enough scans for the confidence interval to be meaningful.","min_scans_for_grade":15,"confidence_levels":{"low":30,"medium":100}},"timestamps":{"description":"The `scanned_at` value displayed on a scan and used for time-window aggregations (consolidation, daily snapshots, popularity 30j) is set SERVER-SIDE at ingestion time (`now()` on the Pulse server), not by the WordPress plugin. Trusting the plugin's local clock would let any site with a drifting system clock skew its scans into the future or the past on the public page. The plugin's local timestamp is still captured separately in `client_scanned_at` but is only used as an idempotency key \u2014 a network retry of the same payload must stay a no-op, and the only deterministic identifier for that retry is the original local clock value the plugin captured."},"consolidation":{"description":"To prevent the public score from resetting every time a plugin or theme ships a new version (which would make actively maintained plugins permanently appear as 'no data'), the score on a public page is computed over a SLIDING TIME WINDOW. All scans `usableForAggregation` for a (plugin, page_type) pair within the window contribute to the consolidated median, regardless of the version they were collected on. Per-version medians remain available in the history table for audit and trend analysis, but the headline score on the page reflects the rolling window.","window_days":90,"storage":"plugin_consolidated_stats \/ theme_consolidated_stats (one row per (owner, page_type)).","label_version":"The most recent version represented in the window is surfaced as the headline label (e.g. \"v4.1.0\") so the reader can see which release is currently being measured, while the score itself stays stable.","rationale":"A new release ships \u2192 the latest version starts at 0 scans \u2192 without consolidation, the board would either flip to 'no data yet' or jump on a single early data point. With a 90-day window, a plugin that releases every two weeks naturally aggregates ~6 versions, smoothing out the transient and remaining responsive to real performance regressions on recent releases."},"privacy":{"description":"Per-component `private: true` flag (methodology v3.2, opt-in via the plugin UI). When set, Pulse still computes the score and returns it to the plugin so the local report can be displayed, but the component is NOT persisted in plugin_scans \/ theme_scans, NOT registered in the public Plugin\/Theme catalogue, and NOT aggregated into any public median. Lets users keep proprietary or in-house components out of the global ranking while still measuring them locally.","scopes":["scan-level (themes and plugins together)","component-level (per plugin\/theme entry)"]}},"docs_url":"https:\/\/www.wpfootprint.com\/methodology","source_files":{"calculator":"app\/Services\/Scoring\/FootprintScoreCalculator.php","aggregator":"app\/Services\/Scoring\/PassAggregator.php","baseline":"app\/Services\/Scoring\/BaselineGuards.php","plausibility":"app\/Services\/Validation\/PlausibilityChecker.php","ingestion":"app\/Services\/Ingestion\/ScanIngestionService.php","config":"config\/pulse.php"}}