textmachine/backend/internal/pipeline/stopcontest.go

103 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pipeline
import (
"fmt"
"strconv"
"strings"
)
// stopcontest.go: the POST-CALL half of «contested» — the question asked of a row AFTER the role has
// answered and before anyone would pay a second model for a second opinion.
//
// It is a SEPARATE predicate from the pre-call one (terminology.Candidate.Contest) because it reads
// different fields at a different moment, and folding them into one would make the population unreadable:
// a share of «contested» that mixes «the drafts disagreed» with «the role was unsure» answers neither
// question. The two also route differently — the pre-call one decides who is ASKED and with what context,
// this one decides who is asked AGAIN.
//
// ⛔ WHAT THE POPULATION OF THESE SIGNALS IS ON EVERYTHING THIS PROJECT HAS BOUGHT, so a caller does not
// mistake a threshold for a filter: on both cold runs the conflict signals are EMPTY — `contradicts` 0 of
// 69 and 0 of 66, `bank_holds` the same, and no row came back unresolved — while `conf` is present on
// every row and `invented` on 11 of 69 (A) and 17 of 66 (B). A predicate built on conflicts alone would
// select nothing at all here and could not be checked against this material; that is a fact about the
// material, not about the signal.
// StopContestOpts are the post-call predicate's inputs. As with the pre-call opts, this file holds no
// default for any of them: the numbers are what the $0 probe varies (cmd/tmbankprobe), and a paid consumer
// brings its own with the measurement that chose it.
type StopContestOpts struct {
// UseConf turns the confidence arm ON; MaxConf is then the threshold a row's role-stated confidence
// must be at or below to count as contested.
//
// ⛔ TWO FIELDS AND NOT A SENTINEL, because the sentinel made the ZERO VALUE of this struct an ACTIVE
// arm: with «-1 disables», a caller that filled nothing selected every row whose reply stated 0 % —
// the most urgent rows on the sheet — while believing it had asked for nothing. A file that declares
// «no defaults live here» may not have a zero value with behaviour.
//
// A NEGATIVE Conf on the ROW means the reply carried no confidence at all, and never counts as low:
// «the role said nothing» is not «the role said it was unsure».
//
// ⛔ THE RATIFIED LIMIT OF THIS NUMBER (D39.102 п.2), and it is narrow: the verbalized confidence may
// order a review queue INSIDE one model's reply (measured AUC 0.770.85). It is not an arbiter, not a
// weight, and not comparable between models. Using it here to decide WHO IS LOOKED AT AGAIN stays
// inside that; letting it decide WHICH RENDERING WINS, or comparing it across two models' replies,
// would not, and nothing in this file does either.
UseConf bool
MaxConf int
// CountUnresolved: the role produced no rendering at all (declined, or nothing usable came back).
CountUnresolved bool
// CountInvented: the consolidated rendering is not one the drafts proposed. Legitimate — the role sees
// the whole book and the drafts saw fragments — and the class an owner is told to read first.
CountInvented bool
// CountConflicts: the rendering contradicts another of this run's consolidations, or a row the bank
// already holds for the same firing surface.
CountConflicts bool
// CountPreCall carries the pre-call verdict into the post-call one: the drafts disagreed and the role
// has now picked a side. Off by default (the zero value) so the two populations can be measured apart.
CountPreCall bool
MinConventions int // used only when CountPreCall is set
}
// StopContest is the verdict plus the signals that produced it, in a fixed order so two runs of the probe
// print the same row the same way.
type StopContest struct {
Contested bool
Signals []string
}
// Contest applies the post-call predicate to a stop row.
//
// ⚠ A row the bank SETTLED (never asked) is never contested: there is no answer to doubt. Reading it as
// contested would put the cheapest rows — the ones a seed already decided — at the head of the queue for a
// second paid opinion.
//
// ⛔ A ROW THE BASIS SETTLED IS A DIFFERENT CASE AND IS NOT EXEMPT, and that is DECIDED here rather than
// inherited from the name next to it. SettledByBank means the bank renders the surface and every draft
// agreed — there is genuinely nothing to doubt. A basis row carries a real rendering, chosen by a paid
// role in an earlier purchase, and neither half of the other sentence is true of it: the bank holds no
// seed surface for it and the drafts were never asked. So it is judged on the same signals as any other
// answered row — the doubt it deserves is the doubt its own rendering deserves. Exempting it by the
// resemblance of the two field names would silence the queue for most of a settled book.
func (r BankStopRow) Contest(o StopContestOpts) StopContest {
var out StopContest
if r.SettledByBank {
return out
}
if o.CountUnresolved && strings.TrimSpace(r.Dst) == "" {
out.Signals = append(out.Signals, "unresolved")
}
if o.UseConf && r.Conf >= 0 && r.Conf <= o.MaxConf {
out.Signals = append(out.Signals, "conf<="+strconv.Itoa(o.MaxConf))
}
if o.CountInvented && r.Invented {
out.Signals = append(out.Signals, "invented")
}
if o.CountConflicts && (len(r.Contradicts) > 0 || len(r.BankHolds) > 0) {
out.Signals = append(out.Signals, "conflict")
}
if o.CountPreCall && o.MinConventions > 0 && r.Conventions >= o.MinConventions {
out.Signals = append(out.Signals, fmt.Sprintf("conventions>=%d", o.MinConventions))
}
out.Contested = len(out.Signals) > 0
return out
}