Senior Application Developer

loc: Maynooth, ON K0L 2S0tel: 613-553-0960email: rgsamways@gmail.com
Robin Samways

Farpost · Reputation Scoring

$ Scoring a Blank Slate Without Lying About It

Reputation-based dispatch ranks candidates by their track record — but a brand-new professional with zero history has no track record to rank. The naive fix is to score every unknown dimension as a flat, neutral 0.5 and call it fair. Farpost doesn't do that.

Each signal gets its own honest floor instead: recent activity floors at 0.3, not 0.5 — someone with no activity history isn't assumed to be exactly average, they're assumed slightly below it until proven otherwise. Engagement and experience floor at 0.0, since there's nothing to credit yet. The one place a flat 0.5 does appear is deliberately narrow: only when the reputation computation itself throws an exception, which is a different situation — a broken calculation, not an absence of history — and the code says so in a comment.

async def rank_candidates(candidates: list[Professional]) -> list[Professional]:
    """Rank candidates by reputation signals. Role-agnostic.

    Uses the reputation timeline read-model. Professionals with no history are not
    flatly neutral — each signal has its own floor: recently_active=0.3,
    engagement=0.0, experience=0.0, availability computed as normal from real
    capacity fields. Only the `except` fallback below (a timeline-computation
    error, not "no history") uses a flat 0.5.
    """
    scored = []
    for pro in candidates:
        try:
            timeline = await get_reputation_timeline(pro.slug)
            s = timeline.signals
            if s.quarantined:
                continue  # provisional role — ineligible for dispatch
            availability = (
                (pro.dispatch_capacity_max - pro.dispatch_capacity_current)
                / pro.dispatch_capacity_max
                if pro.dispatch_capacity_max > 0 else 0.5
            )
            experience = min(s.total_events, 20) / 20.0
            recently = 1.0 if s.recently_active else 0.3
            engagement = min(s.distinct_relationship_types, 5) / 5.0
            score = (
                recently * 0.30 + engagement * 0.25 + experience * 0.20
                + availability * 0.15 + 0.10  # base — everyone gets a floor
            )
            scored.append((round(score, 4), pro))
        except Exception as exc:
            scored.append((0.5, pro))  # timeline computation failed — NOT "no history"

    scored.sort(key=lambda t: t[0], reverse=True)
    return [pro for _, pro in scored]

The fix

recently_active is boolean, so it resolves to 1.0 or the 0.3 floor, never an average. experience and engagement scale linearly against a capped denominator (20 events, 5 relationship types) so they floor naturally at 0.0 with no history. Only the except branch — an actual computation failure — falls back to a flat 0.5, and that fallback is deliberately distinguished, in a comment, from what "no history" should mean.

Why this matters

Conflating "we have no data" with "something broke" is a common, easy-to-miss scoring-system bug — both look like a missing number, but they should never resolve to the same score. Treating them differently, and documenting why in the code, is the kind of small, deliberate honesty about what a metric actually means that separates a scoring system that's technically working from one that's actually fair.

Feedback on this page