textmachine/backend/internal/terminology/contested.go

83 lines
4.2 KiB
Go

package terminology
// contested.go: the PRE-CALL half of the word «contested» — the question asked of a candidate BEFORE any
// money moves, on nothing but what the drafts already produced.
//
// WHY THE WORD NEEDS A PREDICATE AT ALL. The mechanisms the owner asked for — a reviewer of another model
// family «only on contested clusters», a retry, a fallback (D39.254 п.2/п.6, backlog rows 435 and 438) —
// all route on «contested», and today that word has no denominator: nobody can say what share of a book's
// candidates it selects, so nobody can say what the routing would COST. A predicate with a measured
// population is the thing that has to exist first; the router is the pack after.
//
// ⛔ THE THRESHOLDS ARE INPUTS, NOT DEFAULTS, and there is no default value anywhere in this file. A
// number baked into Go would be a decision about a PAIR (how much do this language's drafts normally
// disagree) written into the engine every pair shares, and it would be a decision nobody measured. The
// caller passes them; today the only caller is the $0 probe that measures the population across a grid of
// them (cmd/tmbankprobe). When a paid consumer appears, its thresholds arrive as configuration carrying
// the measurement that chose them.
//
// ⚠ WHAT THIS PREDICATE CAN AND CANNOT SEE. It runs where buildBankCandidates runs — before the classifier
// — so the candidate's TYPE is still the miner's heuristic. It therefore answers «contested BY THE DRAFTS»
// and never «contested by type». The other half of the word lives after the answer comes back
// (pipeline.BankStopRow.Contest).
// ContestOpts are the pre-call predicate's inputs.
type ContestOpts struct {
// MinConventions is how many distinct DECISIONS the drafts must have made for the candidate to be in
// contention at all. Conventions, not Spread: two chunks writing «Море истинной ци» and «море истинной
// ци» made ONE decision written two ways, and counting that as a contest would send a normalization nit
// to a second model. Below 2 the arm is off — every candidate qualifies on this axis.
MinConventions int
// MaxLeaderShare is the dominance arm: the share of draft chunks behind the BEST-ranked convention,
// above which the contest is considered settled by the drafts themselves (0.75 = three quarters of the
// chunks already agree). Zero or negative disables the arm, and then MinConventions decides alone.
MaxLeaderShare float64
}
// Contest is what the predicate saw, not only what it decided: a bare bool cannot be argued with, and the
// numbers below are what a threshold is chosen against.
type Contest struct {
Contested bool
Conventions int
// LeaderShare is the best-ranked convention's share of the draft chunks, in [0,1]. It is 1 for a single
// convention, and 0 when the drafts proposed nothing at all — see Unsupported, which is the case that
// must not be read as «the drafts agreed».
LeaderShare float64
// Unsupported marks a candidate no draft rendered at all: the role is asked to render it with nothing
// to rank. It is NOT a contest — there is no disagreement — and it is carried separately because a
// router that treats it as agreement would send the least-evidenced rows to the cheapest path.
Unsupported bool
}
// Contest applies the pre-call predicate to a candidate.
func (c Candidate) Contest(o ContestOpts) Contest {
out := Contest{Conventions: c.Conventions()}
if len(c.Variants) == 0 {
out.Unsupported = true
return out // no drafts, no contest: nothing disagreed with anything
}
total, top := 0, 0
for _, v := range c.Variants {
total += v.Chunks
if v.Chunks > top {
top = v.Chunks
}
}
switch {
case total > 0:
out.LeaderShare = float64(top) / float64(total)
default:
// Renderings with no chunk count behind them (a channel that carries the rendering but not its
// support). Reported as 0 rather than as 1: «nothing is behind the leader» is what happened, and
// calling it dominance would silently settle every such contest.
out.LeaderShare = 0
}
if out.Conventions < o.MinConventions {
return out
}
if o.MaxLeaderShare > 0 && out.LeaderShare > o.MaxLeaderShare {
return out // the drafts already agree by a margin the caller called decisive
}
out.Contested = true
return out
}