NAME

Test::CPAN::Health::Report - Aggregated results and overall score for a distribution health run

SYNOPSIS

use Test::CPAN::Health::Report;

my $report = Test::CPAN::Health::Report->new(checks => \@check_objects);

$report->add_result($result);

printf "Score: %d/100\n", $report->overall_score;

for my $result (@{ $report->results }) {
    printf "  %s: %s\n", $result->check_id, $result->status;
}

DESCRIPTION

Holds all Test::CPAN::Health::Result objects produced by a run and computes the weighted overall score.

The scoring formula is a weighted mean of per-check scores:

score = sum(result.score * check.weight)
        ---------------------------------
        sum(check.weight)

for all results where score is defined and status is not 'skip'

Hard caps are applied after the weighted mean: if the SecurityAdvisories check fails, the overall score is capped at 60; if CPANTesters fails, it is capped at 75. This prevents a distribution with known CVEs or a poor CI pass rate from achieving a misleadingly high headline score.

LIMITATIONS

  • Checks added after overall_score has been called are included on the next call (the score is computed lazily and cached until invalidated by add_result).

add_result

PURPOSE

Append a single Result to the report and invalidate the cached score.

API SPECIFICATION

INPUT

result  Test::CPAN::Health::Result  required

OUTPUT

Returns $self to allow chaining.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
RPT01 | FATAL    | result must be a Result object     | Pass a Result instance

FORMAL SPECIFICATION

-- Z schema (placeholder) --
AddResultOp
Report
Report'
result? : Result
-------------------------------------------------------
Report'.results = Report.results ++ [result?]
Report'.score_dirty = true

SIDE EFFECTS

Invalidates the cached overall score.

USAGE EXAMPLE

$report->add_result($result)->add_result($other_result);

overall_score

PURPOSE

Compute and return the weighted overall health score in the range 0..100. The result is cached until add_result invalidates it.

Hard caps are applied last: a failing SecurityAdvisories caps the score at 60; a failing CPANTesters caps it at 75. Both caps may apply simultaneously (the lower cap wins).

API SPECIFICATION

INPUT

None.

OUTPUT

Integer in the range 0..100.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
      |          |                                    |

FORMAL SPECIFICATION

-- Z schema (placeholder) --
OverallScoreOp
results : seq Result
weights : check_id --> N1
score   : 0..100
-------------------------------------------------------
let scorable == {r : results | r.score /= undefined /\ r.status /= skip}
score = floor(sum{r : scorable @ r.score * weights(r.check_id)}
             / sum{r : scorable @ weights(r.check_id)})
security_advisories_fail => score <= 60
cpan_testers_fail        => score <= 75

SIDE EFFECTS

None.

USAGE EXAMPLE

printf "%d/100\n", $report->overall_score;

results

PURPOSE

Returns all Result objects in insertion order.

API SPECIFICATION

INPUT

None.

OUTPUT

Arrayref of Test::CPAN::Health::Result objects.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
      |          |                                    |

FORMAL SPECIFICATION

-- Z schema (placeholder) --
results : seq Result

SIDE EFFECTS

None.

USAGE EXAMPLE

for my $r (@{ $report->results }) { ... }

by_status

PURPOSE

Group results by their status string.

API SPECIFICATION

INPUT

None.

OUTPUT

Hashref mapping status strings to arrayrefs of Result objects.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
      |          |                                    |

FORMAL SPECIFICATION

-- Z schema (placeholder) --
by_status : status --> seq Result
-------------------------------------------------------
forall s : dom(by_status) @ forall r : by_status(s) @ r.status = s

SIDE EFFECTS

None.

USAGE EXAMPLE

my $failures = $report->by_status->{fail};

by_category

PURPOSE

Group results by the category of the originating check. Requires that each Result's data hashref carries a category key (populated by the Runner when it adds results).

API SPECIFICATION

INPUT

None.

OUTPUT

Hashref mapping category strings to arrayrefs of Result objects.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
      |          |                                    |

FORMAL SPECIFICATION

-- Z schema (placeholder) --
by_category : category --> seq Result

SIDE EFFECTS

None.

USAGE EXAMPLE

my $security_results = $report->by_category->{security};

as_hash

PURPOSE

Serialise the entire report to a plain hashref (for JSON reporter).

API SPECIFICATION

INPUT

None.

OUTPUT

Hashref with keys: overall_score, pass, warn, fail, skip, error, results.

MESSAGES

Code  | Severity | Message                            | Resolution
------+----------+------------------------------------+---------------------
      |          |                                    |

FORMAL SPECIFICATION

-- Z schema (placeholder) --
AsHashOp
report : Report
output : Hashref

SIDE EFFECTS

None.

USAGE EXAMPLE

my $href = $report->as_hash;

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

LICENSE AND COPYRIGHT

Copyright (C) 2025-2026 Nigel Horne.

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.