| """The scorer interface — the single seam between the UI and any scoring backend. |
| |
| A `Scorer` turns a `ParsedExport` into a `ScoreResult` (5 axis scores 0-10, critical-type |
| counts, per-axis confidence, evidence quotes, tips, one improvement line). `score_to_card` |
| maps that result onto the UI's `CardData`. The real ML model implements `Scorer.score` with |
| the same signature; nothing in the UI changes. |
| """ |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from typing import Protocol |
|
|
| from ..data import AXES, Axis, CardData, CriticalBreakdown |
| from ..parsing import ParsedExport |
|
|
|
|
| @dataclass |
| class AxisScore: |
| name: str |
| score: float |
| confidence: str |
| evidence: list[str] |
| tip: str |
|
|
|
|
| @dataclass |
| class ScoreResult: |
| axes: list[AxisScore] |
| critical_counts: dict[str, int] |
| improvement: str |
|
|
|
|
| class Scorer(Protocol): |
| def score(self, parsed: ParsedExport, progress=None) -> ScoreResult: |
| ... |
|
|
|
|
| def score_to_card(result: ScoreResult, name: str) -> CardData: |
| """Adapt a backend-agnostic ScoreResult into the UI's CardData.""" |
| c = result.critical_counts |
| return CardData( |
| name=(name or "").strip() or "Anonymous", |
| axes=[Axis(a.name, round(a.score, 1), a.confidence, a.evidence, a.tip) for a in result.axes], |
| critical=CriticalBreakdown( |
| skepticism=c.get("skepticism", 0), |
| source_req=c.get("source_req", 0), |
| rebuttal=c.get("rebuttal", 0), |
| verify=c.get("verify", 0), |
| re_ask=c.get("re_ask", 0), |
| ), |
| improvement=result.improvement, |
| ) |
|
|