Documentation
¶
Overview ¶
Package eval scores a finished session.
§14.3 lists nine metrics. Four of them cannot be computed yet — three need components that arrive in M4 and M8, one needs labelled answers — and this package reports that rather than omitting them. A scorecard showing five numbers reads as a complete picture; a scorecard showing five numbers and four explicit "not measured" lines reads as what it is.
Everything here is mechanical: pure arithmetic over persisted state, no model calls, no network, no judgement. That is the whole point. These are the numbers that can block a merge without anyone arguing about them, and they work today against a session `mole research` already wrote.
Index ¶
- func LabelsFrom(ps *PairSet) map[string]string
- func WritePairs(path string, ps *PairSet) error
- type Agreement
- type CitationProblem
- type CitationReport
- type Corpus
- type CorpusReport
- type Delta
- type DumpOptions
- type JudgeOptions
- type LabelledPair
- type Metric
- type Options
- type PairScore
- type PairSet
- type PipelineReader
- type Question
- type QuestionResult
- type Scorecard
- type SourceReader
- type Status
- type StoredReader
- type Verdict
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func LabelsFrom ¶
LabelsFrom extracts the labels already applied to a set, for carrying forward.
func WritePairs ¶
WritePairs writes a set for labelling.
Types ¶
type Agreement ¶
type Agreement struct {
Compared int `json:"compared"`
Same int `json:"same"`
// Unanswered counts pairs one side or the other did not judge.
Unanswered int `json:"unanswered"`
// Confusion[a][b] is how often the first set said a and the second said b.
Confusion map[string]map[string]int `json:"confusion"`
// SameEffect counts pairs where the two verdicts do the same thing to the graph,
// whether or not they used the same word. See verifier.Relation.EffectOf: five
// relations collapse to three effects, because "supports", "refines" and
// "unrelated" are all read by nothing.
//
// This is the number to judge a judge by, and it is not a softer version of
// Rate. Measured on claude-haiku-4-5 over 37 pairs judged twice: 84% raw, 97%
// by effect. Five of the six disagreements were supports/refines/unrelated
// shuffles that change nothing downstream; one was contradicts-vs-supports,
// which changes a confidence score and queues a research lead. Acting on 84%
// would mean rewriting a prompt to chase six problems when there was one.
SameEffect int `json:"same_effect"`
}
Agreement is how often two verdict sets say the same thing.
func CompareVerdicts ¶
Compare two verdict sets over the pairs both judged.
The cheapest useful measurement of a judge, and the only one that needs no labels: run the SAME model over the SAME pairs twice and see how often it agrees with itself. A judge that does not is not measuring anything, and no quantity of labelling will fix it — qwen2.5:3b re-judging sixteen of its own contradictions kept three of them, calling seven "unrelated" and four "supports".
Self-consistency is a ceiling, not a score: a judge cannot be more accurate than it is reproducible. Screening on it first is far cheaper than labelling.
func (Agreement) EffectRate ¶
EffectRate is agreement on what the graph will do, ignoring vocabulary.
Always at least Rate: identical verdicts have identical effects.
type CitationProblem ¶
type CitationProblem struct {
Verdict Verdict `json:"verdict"`
Source string `json:"source"`
Claim string `json:"claim"`
Detail string `json:"detail"`
}
CitationProblem is one claim that did not verify.
type CitationReport ¶
type CitationReport struct {
Verified int `json:"verified"`
Mismatch int `json:"mismatch"`
OffsetDrift int `json:"offset_drift"`
Unreachable int `json:"unreachable"`
Skipped int `json:"skipped"`
Problems []CitationProblem `json:"problems,omitempty"`
}
CitationReport aggregates the pass.
func VerifyCitations ¶
func VerifyCitations(ctx context.Context, claims []*core.Claim, skipSources map[string]bool, r SourceReader) CitationReport
VerifyCitations re-reads each cited source and looks for the quote.
skipSources names URLs whose text came from the search provider rather than a fetch. Re-fetching those would run a different extractor over the same page and disagree on whitespace and boilerplate, producing mismatches that say nothing about the claim.
func (CitationReport) Checked ¶
func (r CitationReport) Checked() int
Checked is the number of claims the metric could actually judge. Unreachable and skipped sources are excluded: counting them would let a run with no network report perfect accuracy.
type CorpusReport ¶
type CorpusReport struct {
Corpus string `json:"corpus"`
Results []QuestionResult `json:"results"`
// Aggregate holds the mean of every metric measured on at least one question.
Aggregate []Metric `json:"aggregate"`
}
CorpusReport is a whole run.
func (CorpusReport) Failed ¶
func (r CorpusReport) Failed() bool
Failed reports whether the run should block a merge.
Any hard regression, any question that could not run, and any question that ran and produced nothing. Quality is deliberately not part of it: without labelled answers there is no defensible threshold, and a gate that fires on a judgement call gets disabled within a week. "Zero claims" is not a judgement call.
type Delta ¶
type Delta struct {
Name string `json:"name"`
Baseline float64 `json:"baseline"`
Current float64 `json:"current"`
Change float64 `json:"change"`
Unit string `json:"unit,omitempty"`
}
Delta is one metric's movement against a baseline.
func Compare ¶
Compare reports how the aggregate moved against a stored baseline.
Reports movement and says nothing about whether it is good. Without labelled answers there is no direction of improvement for most of these — a lower disagreement rate is better if the adjudicator was producing false positives and worse if it has stopped finding real ones — and a tool that guesses at that will be believed.
type DumpOptions ¶
type DumpOptions struct {
// Kinds restricts the dump to pairs the model gave these verdicts. Empty means every
// pair that carries an edge.
Kinds []string
// All includes pairs with no edge — the ones the model called unrelated.
//
// Off by default because they dominate: a 25-claim session produces a few dozen
// edges and several hundred candidate pairs, and a labelling task nobody finishes
// measures nothing. Turn it on to measure RECALL — whether real disagreements were
// missed — once precision is known.
All bool
// MaxCandidatesPerClaim mirrors the Verifier's retrieval cap, so the candidate space
// is regenerated exactly as the run saw it.
MaxCandidatesPerClaim int
}
DumpOptions filter what is written.
type JudgeOptions ¶
type JudgeOptions struct {
Model string
BatchSize int
// Kinds restricts which stored verdicts get re-judged. Empty means all of them.
Kinds []string
// Labels carries labels across from an earlier set, matched by pair key.
//
// The reason the whole harness is worth having: label once, and every future model
// or prompt is scored against the same judgements rather than needing the work done
// again.
Labels map[string]string
}
JudgeOptions configure a re-judge.
type LabelledPair ¶
type LabelledPair struct {
// Pair is the canonical key, stable across dumps of the same session.
Pair string `json:"pair"`
A string `json:"a"`
B string `json:"b"`
SourceA string `json:"source_a,omitempty"`
SourceB string `json:"source_b,omitempty"`
// Model is what the adjudicator said. "unrelated" means no edge was stored, which
// covers both "judged unrelated" and "never reached" — Judged distinguishes them.
Model string `json:"model"`
// Baseline is what the STORED graph said, when this set came from re-judging with a
// different model. Kept so a comparison shows both verdicts on one line.
Baseline string `json:"baseline,omitempty"`
// Why is the model's own rationale, which is often the clearest evidence that a
// verdict is wrong.
Why string `json:"why,omitempty"`
// Judged is false when one of the claims was never verified, so the pair was never
// put to the model at all. Scoring skips those: they say nothing about the judge.
Judged bool `json:"judged"`
// Label is the truth, filled in by hand. Empty means unlabelled.
Label string `json:"label"`
// Note is for the labeller.
Note string `json:"note,omitempty"`
}
LabelledPair is one judgement, before and after a human looks at it.
type Metric ¶
type Metric struct {
Name string `json:"name"`
Status Status `json:"status"`
// Value and Unit are set when Status is Measured.
Value float64 `json:"value,omitempty"`
Unit string `json:"unit,omitempty"`
// Detail is the arithmetic behind the number, so a reader can check it.
Detail string `json:"detail,omitempty"`
// Reason explains a Blocked status: what is missing and which milestone
// brings it.
Reason string `json:"reason,omitempty"`
// Regression marks a metric that should fail a build. Only ever set on
// metrics that are objectively wrong, never on quality judgements.
Regression bool `json:"regression,omitempty"`
}
Metric is one line of the scorecard.
func Aggregate ¶
func Aggregate(results []QuestionResult) []Metric
Aggregate summarizes a set of question results.
The MEAN of each metric across the questions that measured it, with the count carried in Detail so a number averaged over two questions is not read as one averaged over forty. Metrics that were blocked everywhere stay blocked rather than becoming 0.
func CitationAccuracyFor ¶
func CitationAccuracyFor(rep CitationReport) Metric
citationAccuracy turns the report into a scorecard line. CitationAccuracyFor and CitationOffsetDriftFor are exported for the tests that pin the two apart: the distinction between "the source never said it" and "the offset moved" is the whole content of this pair, and it was wrong once.
func CitationOffsetDriftFor ¶
func CitationOffsetDriftFor(rep CitationReport) Metric
CitationOffsetDriftFor is the precision half. See CitationAccuracyFor.
type Options ¶
type Options struct {
// Citations, when set, re-reads every cited source and checks the quote is
// actually in it (§14.3 citation accuracy).
//
// Off by default because it costs a fetch per source. Under cassette
// replay it is free and deterministic, which is what §14.1 was built for.
Citations SourceReader
}
Options tune what Score computes.
type PairScore ¶
type PairScore struct {
Labelled int `json:"labelled"`
Unlabelled int `json:"unlabelled"`
// Skipped is pairs that were never put to the model.
Skipped int `json:"skipped"`
Correct int `json:"correct"`
// Confusion[modelVerdict][trueLabel] is how often the model said one thing and the
// truth was another.
Confusion map[string]map[string]int `json:"confusion"`
// Precision and Recall per relation. Precision is what matters for a judge that
// over-reports: of the pairs it called X, how many were X.
Precision map[string]float64 `json:"precision"`
Recall map[string]float64 `json:"recall"`
// CorrectEffect counts verdicts that do the right thing to the graph even when
// they used the wrong word, on the same reasoning as Agreement.SameEffect.
//
// Measured on claude-haiku-4-5 over 37 labelled pairs: 76% raw, 97% by effect.
// Eight of its nine errors were calling an unrelated pair "supports" or
// "refines", which builds an edge nothing reads. The ninth was a false
// contradiction, which costs a confidence penalty and a research lead.
CorrectEffect int `json:"correct_effect"`
}
PairScore is how the adjudicator did against the labels.
func ScorePairs ¶
ScorePairs compares the model's verdicts against the labels.
Unlabelled pairs are counted and excluded, never guessed at — a score computed over whichever pairs someone got round to labelling, reported as the score, is how a test set starts lying.
func (PairScore) EffectAccuracy ¶
EffectAccuracy is accuracy over what the graph will do, ignoring vocabulary.
type PairSet ¶
type PairSet struct {
Session string `json:"session"`
Model string `json:"adjudicator,omitempty"`
Pairs []LabelledPair `json:"pairs"`
}
PairSet is a dumped set awaiting labels.
func DumpPairs ¶
func DumpPairs(ctx context.Context, st store.Store, sessionID string, opts DumpOptions) (*PairSet, error)
DumpPairs regenerates a session's candidate pairs and pairs them with what the adjudicator said.
Retrieval is deterministic and costs nothing, so the candidate space is reproduced rather than stored — which also means a dump taken today reflects today's retriever.
func JudgeSession ¶
func JudgeSession(ctx context.Context, st store.Store, p llm.Provider, sessionID string, opts JudgeOptions, log *slog.Logger) (*PairSet, error)
JudgeSession re-adjudicates a session's pairs with a different model.
Reads the store and writes nothing to it. The stored graph is the thing being compared against, so a tool that overwrote it while measuring it would have nothing left to measure.
type PipelineReader ¶
PipelineReader re-reads a source through the same fetch and extract path the actor used.
Using the same components is the point: a different extractor would produce different whitespace and boilerplate, and every difference becomes a mismatch that is an artefact of the harness rather than a fault in the claim.
func NewPipelineReader ¶
func NewPipelineReader(f fetch.Fetcher, e extract.Extractor) *PipelineReader
type Question ¶
type Question struct {
// ID is stable across runs so a baseline can be compared question by question.
ID string `json:"id"`
Question string `json:"question"`
// Tags group results in the aggregate. §14.2's categories — settled, disputed,
// stale, plausible-but-wrong — are the intended vocabulary, but nothing here
// enforces a list: a corpus that cannot describe its own questions is less useful
// than one with an unexpected tag in it.
Tags []string `json:"tags,omitempty"`
// Notes is for whoever maintains the corpus. Never sent to a model.
Notes string `json:"notes,omitempty"`
}
Question is one corpus entry.
type QuestionResult ¶
type QuestionResult struct {
ID string `json:"id"`
Question string `json:"question"`
Tags []string `json:"tags,omitempty"`
SessionID string `json:"session_id,omitempty"`
// Err is set when the run itself failed. Distinct from a run that produced a poor
// answer: one is a broken pipeline, the other is a research outcome, and folding
// them together is how a corpus reports 100% success on a build that cannot run.
Err string `json:"error,omitempty"`
// Status and Claims are how a corpus tells "ran and found nothing" from "ran".
//
// Neither is visible in the scorecard: its hard regressions are budget overshoot,
// ledger drift and stranded holds, and a session that produced zero claims has none
// of them. The first replay through this runner reported a green tick for a run
// whose planner call had failed outright — the exact regression the runner exists to
// catch, passing its own gate.
Status string `json:"status,omitempty"`
Claims int `json:"claims"`
Card Scorecard `json:"scorecard"`
}
QuestionResult is one question's outcome.
func (QuestionResult) Barren ¶
func (q QuestionResult) Barren() bool
Barren reports whether the question ran but produced nothing worth scoring.
type Scorecard ¶
type Scorecard struct {
SessionID string `json:"session_id"`
Metrics []Metric `json:"metrics"`
// Citations is the per-claim detail behind the citation accuracy metric,
// present only when it was computed.
Citations *CitationReport `json:"citations,omitempty"`
}
Scorecard is one session's evaluation.
type SourceReader ¶
SourceReader re-reads a cited source as text.
type Status ¶
type Status string
Status says whether a metric was computed, and if not, why not.
const ( // Measured means the number is real. Measured Status = "measured" // Blocked means the metric cannot be computed until something lands. // Reported explicitly so a zero is never mistaken for a regression. Blocked Status = "blocked" // NotApplicable means this session had nothing to measure. NotApplicable Status = "n/a" )
type StoredReader ¶
type StoredReader struct {
Store store.Store
SessionID string
// Fallback reads a source the store does not have. Nil means such a source is
// simply unreadable, which is the honest answer when there is no network path
// configured.
Fallback SourceReader
// contains filtered or unexported fields
}
StoredReader answers from the source text mole kept, and falls back to a re-fetch when it has none.
Toolkit mode stores the document a quote was verified against — it has to, since verifying against text the caller supplied proves nothing — and the citation metric was re-fetching anyway. That made the measurement worse in exactly the case the store exists for: a page that has changed or 404'd since reports "unreachable" or an offset drift, while mole is holding the bytes the claim was checked against on disk.
The fallback matters. An autonomous session stores no documents, and a toolkit session's text expires after seven days, so this has to degrade to the network rather than to "unverifiable".
func NewStoredReader ¶
func NewStoredReader(st store.Store, sessionID string, fallback SourceReader) *StoredReader
type Verdict ¶
type Verdict string
Verdict is one claim's citation outcome.
const ( // CitationVerified: the quote is present in the cited source. CitationVerified Verdict = "verified" // CitationMismatch: the source was read and the quote is not in it. This // is the finding the metric exists for. CitationMismatch Verdict = "mismatch" // CitationOffsetDrift: the quote is present, but not where the claim says. // Not a fabrication — the citation is sound — but a later re-verification // that trusts the offset would look at the wrong span. CitationOffsetDrift Verdict = "offset-drift" // CitationUnreachable: the source could not be re-read. Says nothing about // the claim, so it must not count against accuracy. CitationUnreachable Verdict = "unreachable" // CitationSkipped: the source was never fetched in the first place — the // search provider supplied its text — so re-fetching would compare against // a different extraction and manufacture mismatches. CitationSkipped Verdict = "skipped" )