eval

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func LabelsFrom

func LabelsFrom(ps *PairSet) map[string]string

LabelsFrom extracts the labels already applied to a set, for carrying forward.

func WritePairs

func WritePairs(path string, ps *PairSet) error

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

func CompareVerdicts(a, b *PairSet) Agreement

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

func (a Agreement) EffectRate() float64

EffectRate is agreement on what the graph will do, ignoring vocabulary.

Always at least Rate: identical verdicts have identical effects.

func (Agreement) Rate

func (a Agreement) Rate() float64

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 Corpus

type Corpus struct {
	Name      string     `json:"name"`
	Questions []Question `json:"questions"`
}

Corpus is a question set.

func LoadCorpus

func LoadCorpus(path string) (*Corpus, error)

LoadCorpus reads a corpus file.

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

func Compare(baseline, current []Metric) ([]Delta, []string)

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

func ScorePairs(ps *PairSet) PairScore

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) Accuracy

func (s PairScore) Accuracy() float64

Accuracy is the fraction of labelled, judged pairs the model got right.

func (PairScore) EffectAccuracy

func (s PairScore) EffectAccuracy() float64

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.

func LoadPairs

func LoadPairs(path string) (*PairSet, error)

LoadPairs reads a labelled set.

type PipelineReader

type PipelineReader struct {
	Fetch   fetch.Fetcher
	Extract extract.Extractor
}

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

func (*PipelineReader) Text

func (p *PipelineReader) Text(ctx context.Context, rawURL string) (string, error)

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.

func Score

func Score(ctx context.Context, st store.Store, sessionID string, opts Options) (Scorecard, error)

Score evaluates one session.

func (Scorecard) Failed

func (s Scorecard) Failed() bool

Failed reports whether any metric is a hard regression.

type SourceReader

type SourceReader interface {
	Text(ctx context.Context, rawURL string) (string, error)
}

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

func (*StoredReader) Text

func (r *StoredReader) Text(ctx context.Context, rawURL string) (string, error)

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"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL