Trust Model

Diagnostic Clusters

Group a run’s failures by cause. next

Scorecard.clusters is an additive field that groups a run’s failing and non-scored items by their shared Diagnostic.code, per metric — turning a flat faithfulness = 0.82 into an actionable failure mode (“the 18% that failed are all missing-citation cases”). It adds explanatory structure only: it carries no value, changes no verdict, and manufactures no authority. Built in the framework; not yet in the version the plugin installs.

A metric value says that something is wrong, not what. Clusters answer “which items failed, and do they share a cause?” by grouping by explicit diagnostic code — never by list order. This is a different axis from the call-identity metrics explorer: clusters group failure instances by cause; the explorer would map a score to its source call site. Those are kept separate (see below).

The DiagnosticCluster

A cluster is one failure mode: the items of metric whose diagnostics share code. Each score contributes once per code (duplicate codes on one score count the item once). It carries no score value — a blocked / non_evaluable / error item is grouped by its cause and counted, never coerced to 0.0.

FieldTypeMeaning
metricstrThe metric these failing/non-scored items belong to.
codestrThe shared Diagnostic.code — the cause the items have in common.
severityinfo · warning · errorThe representative severity (the code’s highest, then least message).
countint ≥ 0How many items (scores) carried this code — not a value.
messagestrThe representative message for the code.

Order-invariant by design

The pure, stdlib-only cluster(scores) function groups by (metric, code) and sorts the result canonically — severity descending, then count descending, then (metric, code). Two runs over the same scores in any order produce an equal list. That order-invariance is what lets the engine store the clusters on the Scorecard and still have them recomputed exactly on load.

Which items is the explorer’s axis, not the cluster’s A cluster deliberately carries no example_ids: it names the failure mode (cause + count + severity). Drilling down to which items is the call-identity job of view --by-call — kept off the cluster so the recompute stays robust when a score’s identity is absent (an older artifact).

The recompute guard

Scorecard.clusters is emitted only when non-empty, so a pre-P3 scorecard.json stays byte-identical and is parsed absent→[]. On load, _verify_consistency recomputes the clusters from the persisted scores and rejects any mismatch: a fabricated or hand-edited cluster fails closed, exactly like a tampered aggregate. A record with no clusters is not checked (the additive-field convention) — dropping the clusters loses explanatory structure only, never a value or a verdict.

A rendering, not a second verdict path Clusters render as sub-rows in both report.md (a “Failure clusters” section) and report.html (<code> · N cases per metric) from the typed field only — no re-grouping, no authority. scorecard.json stays the one source of truth.

A different axis from the explorer

Diagnostic clusters group failure instances by cause (next). They are a different axis from two things they are easily confused with, and neither is badged the same way:

ConstructGroups byStatus
Diagnostic clustersshared Diagnostic.code (failure mode)next
Call-identity “metrics explorer”a score’s source call site / call identityunbuilt
Per-source-function viewthe discovered source function that produced the callnon-goal

In one sentence: clusters group failures by Diagnostic.code and are next; the call-identity metrics explorer would map a score to its source call site and is unbuilt; the per-source-function view (ADR 0037) is a deliberate non-goal, never “planned.”

An illustrative cluster

Two of eleven faithfulness items failed for the same reason — grouped by cause, counted, and carrying no value:

json

{
  "metric": "faithfulness",
  "code": "missing_citation",
  "severity": "error",
  "count": 2,
  "message": "Claim not supported by any retrieved passage."
}

Illustrative example, not a measured result. The cluster tells you the shared cause and how many items hit it; it does not change any score and it does not gate. A run showing clusters is still informational unless a metric is authorised to gate.

Next steps