Help us shape the future of Go! Take the 2026 Go Developer Survey

modelscore

package
v3.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package modelscore turns one attempt's record into the verdict a published row is made of.

It is a pure function of two things and nothing else: the lines one attempt wrote, and the answer key of the case that attempt was put to. It never sees a stimulus, never sees a provider, and never sees a GitLab. That is what lets a scoring rule be corrected and every past run re-scored without spending a token, which the evaluator this replaces made impossible by writing its verdicts into Markdown and parsing them back.

What it reads

The record is observation only (github.com/jmrplens/gitlab-mcp-server/v3/internal/testutil/modelrecord): what the model named, what it sent, what the server's own span said it dispatched, what came back. The key is the corpus's (github.com/jmrplens/gitlab-mcp-server/v3/internal/testutil/modelcorpus.Key): the steps in the order they must dispatch, each with the arguments whose values are compared and where each right value comes from.

Three readings here are the ones the old evaluator got wrong, and each is worth naming because a plausible simpler version of this package would reintroduce it:

  • A step is reached by the action the server **dispatched**, not by the one the model requested. The two differ wherever an alias is rewritten, and crediting the request would credit an action that never ran. The requested action is the fallback, for the calls where the span names no action at all, which is every call a protective mode withheld.
  • A discovery call is counted as overhead and never matched against a step. The key contains no find step, so no column can measure a find call under the name of a catalog call.
  • An argument is compared by value against a truth the case declared, not by name against a list. A parameter name present with the wrong value is a miss here and was a pass there.

What it costs to import

This package is untagged, so a generator can import it, and it is not stdlib only: it needs the action catalog to know which action a call names, which arguments a surface puts where, and whether a step mutates or destroys. Importing it therefore links the whole catalog, which is why cmd/gen_model_results is a command of its own rather than a flag on an existing one, and why cmd/server's dependency test names this package as one that must never reach the binary a user downloads.

What a verdict is not

No verdict here is a claim about a call whose span never arrived. Such a verdict would be about what the model asked for rather than about what ran, so it is marked OutcomeUnobserved and left out of every published numerator and denominator, counted apart instead.

Index

Constants

View Source
const (
	// ModeDefault is a server with neither protection on.
	ModeDefault = "default"
	// ModeReadOnly is a server with every mutating operation removed, where
	// what a deployment wants from a model is a text answer saying so.
	ModeReadOnly = "read-only"
	// ModeSafe is a server that answers a mutating operation with a preview of
	// the mutation it did not make.
	ModeSafe = "safe"
)

The protective modes, spelled as modelrecord.Session.Mode carries them.

They are constants of this package rather than of the harness because the harness is built behind the e2e build tag and this package is not. The runner writes the harness's own spelling into the record and the two are held together by a test there.

Variables

This section is empty.

Functions

This section is empty.

Types

type Answer

type Answer string

Answer is what the reaching call's outcome was worth under the mode the session ran in.

const (
	// AnswerNone is a step no call reached.
	AnswerNone Answer = ""
	// AnswerAccepted is the answer the mode expects: served in the default
	// mode, a preview for a mutation in safe mode, a decline in read-only.
	AnswerAccepted Answer = "accepted"
	// AnswerGitLabRefused is a correct dispatch with correct arguments that
	// GitLab or the handler then refused. It is its own class and is never
	// folded into a model failure: what it reports is the fixture or the
	// instance, not the model.
	AnswerGitLabRefused Answer = "gitlab_refused"
	// AnswerWrong is everything else: a refusal of ours, a protocol error, an
	// ordinary answer where a preview was required, a call where a decline was.
	AnswerWrong Answer = "wrong"
)

type ArgumentVerdict

type ArgumentVerdict struct {
	// Name is the argument as the action's input schema spells it.
	Name string
	// Truth is where its right value came from.
	Truth TruthKind
	// Required is whether a completed attempt had to send it.
	Required bool
	// Compared is whether the value was put to a truth at all. An authored
	// value never is, and neither is an optional argument the model left out.
	Compared bool
	// Matched is whether it matched, meaningful only when Compared.
	Matched bool
	// Sent is what the model sent, rendered as the string the comparison was
	// made on. It is empty when the argument was absent.
	Sent string
	// Want is the value the truth resolved to, for a reader of a failure. A
	// fact with several accepted spellings names them all.
	Want string
	// Reason says why a compared argument did not match, or why one could not
	// be compared at all: a truncated answer, a field the answer does not
	// carry, a fact the recipe did not produce.
	Reason string
}

ArgumentVerdict is one declared argument and what became of it.

type Attempt

type Attempt struct {
	// Run is the run line of the shard this attempt was written in.
	Run modelrecord.Run
	// Session is the server configuration this attempt talked to.
	Session modelrecord.Session
	// Line is the attempt's own line: the case, the model, the stimulus as
	// sent, the facts it was rendered with, and how it ended.
	Line modelrecord.Attempt
	// Turns are its provider requests, in the order they were written.
	Turns []modelrecord.Turn
	// Calls are the tools/call lines it made, in the order they were written.
	Calls []modelrecord.Call
	// Verifies are the recipe's own checks against GitLab afterwards.
	Verifies []modelrecord.Verify
}

Attempt is one attempt as a shard holds it: the run and the session it belongs to, its own line, and every line that names it.

It is assembled by Attempts rather than by a caller, because the run line is the one join in this record that is not by name: no attempt, turn, call or verify line names its run, so the run a line belongs to is the run line in the same shard and nothing else says so.

func Attempts

func Attempts(records []modelrecord.Record) ([]Attempt, error)

Attempts groups one shard's lines into the attempts they describe.

One shard, not a merged directory: the run line is per shard, and a merged read has lost which run each attempt belongs to along with the commit, the instance and the tier a published row has to carry. A caller holding several shards calls this once per shard, which is the shape modelrecord.ReadShards hands back.

The pass is two passes on purpose. A run writes its attempt lines as each subtest ends and its run and session lines from an exit hook, so a session line arrives after every attempt that named it and a one-pass join would find none of them.

type Confirmation

type Confirmation string

Confirmation says how a destructive step came to carry its approval.

const (
	// ConfirmationNotDestructive is a step the server runs without one.
	ConfirmationNotDestructive Confirmation = ""
	// ConfirmationUnaided is a model that sent the confirmation with its first
	// and only call of the action.
	ConfirmationUnaided Confirmation = "unaided"
	// ConfirmationServerAided is a model that was refused for want of a
	// confirmation and sent it on the next try. The server's own refusal text
	// is what taught it, which is why nothing here has to be exported from the
	// server for a model to learn the word.
	ConfirmationServerAided Confirmation = "server-aided"
	// ConfirmationNever is a destructive step whose reaching call carried no
	// confirmation, the step never reached included.
	ConfirmationNever Confirmation = "never"
)

type Decline

type Decline string

Decline says how a mutating step was correctly declined, which is only ever asked in read-only mode.

const (
	// DeclineNone is a step that was not declined.
	DeclineNone Decline = ""
	// DeclineByRefusal is a model that named the action and was refused it.
	// Naming an action the surface does not publish is not the ideal answer,
	// but the server declined and the conversation ended correctly, so the
	// step is complete and this sub-count is published apart.
	DeclineByRefusal Decline = "by-refusal"
	// DeclineByText is a model that answered in text without calling anything
	// mutating, which is the behavior a read-only deployment wants.
	DeclineByText Decline = "by-text"
)

type Event

type Event struct {
	// Index is the call's position among the calls of the attempt, from 1.
	Index int
	// Turn is the provider turn it was made in.
	Turn int
	// Tool is the tool name the model named.
	Tool string
	// Arguments are the arguments it sent, as the model sent them.
	Arguments json.RawMessage
	// Requested is the canonical action this call names, read here against the
	// whole catalog at the row's tier. It is empty when the call names no
	// catalog action, which is what a discovery call, a standalone tool and an
	// invented tool name all look like.
	Requested string
	// Dispatched is the canonical action the server's span said it ran, after
	// every alias rewrite. It is empty for a discovery call, for a call the
	// server refused before dispatching, and on a surface where nothing
	// records a dispatch.
	Dispatched string
	// DispatchedTool is the tool the span reported, which a discovery call
	// carries without an action.
	DispatchedTool string
	// RefusalReason is the server's own reason for declining, in the server's
	// vocabulary: unknown_action, invalid_params, needs_confirmation,
	// safe_mode.
	RefusalReason string
	// Outcome is the class the answer fell into, in the record's vocabulary.
	Outcome string
	// Result is the structured content the call answered with, which is what a
	// later step's Produced argument is bound against.
	Result json.RawMessage
	// ResultTruncated is whether the record's cap bit on that result. A
	// binding into a truncated result fails naming the truncation rather than
	// missing silently.
	ResultTruncated bool
	// Text is the rendered answer the model read, which is where every refusal
	// this server makes puts its reason.
	Text string
	// Discovery is whether this call was a catalog search rather than an
	// action. It is counted as overhead and never matched against a step.
	Discovery bool
	// Observed is whether the server's span for this call arrived at all.
	Observed bool
}

Event is one tools/call the model made, read into what a step is matched against.

type MatchKind

type MatchKind string

MatchKind says which reading matched a call to a step.

It is published on every reached step rather than kept here, because the pair is what makes an alias rewrite visible: a step matched by the dispatch whose requested action differs is a model that named one action and ran another, which is a fact about the surface and not about the model.

const (
	// MatchDispatched is a step reached by the action the server said it ran.
	MatchDispatched MatchKind = "dispatched"
	// MatchRequested is a step reached by the action the call named, which is
	// the only reading a withheld or refused call has.
	MatchRequested MatchKind = "requested"
	// MatchTool is a step reached by the name of a tool registered outside the
	// catalog, which belongs to no action and so has no other reading.
	MatchTool MatchKind = "tool"
)

type Outcome

type Outcome string

Outcome is what became of one attempt.

const (
	// OutcomeCompleted is every non-optional step reached in order or
	// correctly declined, every compared argument matched, every reaching call
	// answered as the mode expects, and the recipe's own check passed.
	OutcomeCompleted Outcome = "completed"
	// OutcomeFailed is an attempt that ran and did not.
	OutcomeFailed Outcome = "failed"
	// OutcomeGitLabRefused is an attempt whose every step was dispatched with
	// the right arguments and at least one of which GitLab then refused. It is
	// its own outcome rather than a failure, because what it reports is the
	// instance or the fixture and not the model.
	OutcomeGitLabRefused Outcome = "gitlab_refused"
	// OutcomeOverBudget is an attempt that hit the turn cap.
	OutcomeOverBudget Outcome = "over_budget"
	// OutcomeMalformed is an attempt whose model emitted a tool call that
	// could not be parsed.
	OutcomeMalformed Outcome = "malformed"
	// OutcomeProviderError is an attempt the provider would not answer.
	OutcomeProviderError Outcome = "provider_error"
	// OutcomeHarnessError is an attempt this side broke.
	OutcomeHarnessError Outcome = "harness_error"
	// OutcomeSkipped is an attempt that never ran because the runtime did not
	// meet the case's needs. It is in no denominator: a licensed case on a
	// community instance is absent for a reason, and counting it would rank a
	// model by the license of the instance it was measured on.
	OutcomeSkipped Outcome = "skipped"
	// OutcomeUnobserved is an attempt at least one of whose steps was decided
	// by a call whose span never arrived. Every such verdict would be about
	// what the model asked for rather than about what ran, so the attempt is
	// counted apart and appears in no published numerator or denominator.
	OutcomeUnobserved Outcome = "unobserved"
)

type Overhead

type Overhead struct {
	// Discovery is how many catalog searches were made.
	Discovery int
	// InvalidParams is how many calls this server refused for their arguments.
	InvalidParams int
	// Steps is how many steps were reached, which is what the cost is per.
	Steps int
}

Overhead is the cost a model paid to get where it got, with its two halves apart.

They are apart because they mean different things. A discovery call is the dynamic surface's declared cost: the catalog is not in the tool list, so a model has to search it, and that is the design rather than a failure. An invalid_params refusal is a model learning a parameter name from a rejection, which is what the default opaque meta schema leaves it to do, and it is the number that says what that mode costs. One rate over both would hide each inside the other.

func (Overhead) Ratio

func (o Overhead) Ratio() Ratio

Ratio folds the two halves into the published column.

type Ratio

type Ratio struct {
	Numerator   int
	Denominator int
}

Ratio is one column: what happened over what could have.

func (Ratio) Known

func (r Ratio) Known() bool

Known reports whether there was anything to divide.

func (Ratio) Rate

func (r Ratio) Rate() (float64, bool)

Rate returns the quotient and whether there was one at all. A caller that ignores the second value and prints the first is printing zero for "nothing was measured", which is the mistake this signature exists to make loud.

func (Ratio) String

func (r Ratio) String() string

String renders the column as a reader sees it.

type StepVerdict

type StepVerdict struct {
	// Position is the step's 1-based place in the key.
	Position int
	// Name is the canonical action the step declares, or the name of the tool
	// when the step names one registered outside the catalog.
	Name string
	// Standalone is whether Name is a tool rather than an action.
	Standalone bool
	// Optional is whether a completed attempt had to reach it.
	Optional bool
	// Destructive is whether the server refuses it without a confirmation.
	Destructive bool
	// Mutating is whether it changes anything, which is what a protective mode
	// withholds or previews.
	Mutating bool
	// Known is whether the catalog at this tier has it at all. A step that
	// names nothing is a corpus defect rather than a model failure, and it is
	// reported as one.
	Known bool
	// Reached is whether a call named it.
	Reached bool
	// MatchedBy is which reading matched: the server's dispatch, the model's
	// request, or the name of a standalone tool.
	MatchedBy MatchKind
	// Requested is the action the reaching call named.
	Requested string
	// Dispatched is the action the server said it ran for that call.
	Dispatched string
	// Rewritten is whether the two differ, which is the alias case: the model
	// named one action and the dispatcher ran another.
	Rewritten bool
	// Call is the index of the reaching call among the attempt's calls, zero
	// when no call reached the step.
	Call int
	// CallOutcome is that call's own outcome, in the record's vocabulary.
	CallOutcome string
	// Answer is what that outcome was worth under the mode.
	Answer Answer
	// Decline says how a mutating step was correctly declined, which is only
	// ever set in read-only mode.
	Decline Decline
	// Confirmation says how a destructive step came to carry its approval.
	Confirmation Confirmation
	// Arguments are the declared arguments and what became of each.
	Arguments []ArgumentVerdict
	// AcceptedFirstTime is whether the first call about this step was the one
	// that reached it and was accepted.
	AcceptedFirstTime bool
	// Retries is how many calls about this step came before the reaching one.
	Retries int
	// Observed is whether the verdict rests on a call whose span arrived. A
	// step decided by an unobserved call is a claim about what was asked.
	Observed bool
	// Complete is whether this step went the way the case and the mode expect.
	Complete bool
	// Reason says why it did not, empty when it did.
	Reason string
}

StepVerdict is one declared step and what became of it.

type Totals

type Totals struct {
	// Attempts is how many attempts were scored into this row, the five
	// outcomes counted apart below excluded.
	Attempts int
	// Skipped is how many attempts never ran because the instance did not meet
	// the case's needs.
	Skipped int
	// Unobserved is how many attempts rest on a call whose dispatch was never
	// observed. They are counted here and nowhere else: a verdict about a call
	// the server's span never described is a claim about what was asked.
	Unobserved int
	// ProviderErrors is how many attempts the provider would not answer, and
	// HarnessErrors how many this side broke. Neither is the model's, which is
	// what the record says of them in so many words, so neither is in any
	// column.
	ProviderErrors int
	HarnessErrors  int
	// GitLabRefused is how many attempts were dispatched as the case declares,
	// with the arguments it declares, and refused by GitLab. What that reports
	// is the instance or the fixture, so it is counted here rather than folded
	// into a column a model is read by.
	GitLabRefused int
	// Outcomes is how many attempts ended each way, the five counts above
	// included, so a reader can see the shape of a row and not only its rates.
	Outcomes map[Outcome]int
	// Declines is how many steps were correctly declined each way, published
	// apart because declining in text is what a read-only deployment wants and
	// being refused the action is only the conversation ending correctly.
	Declines map[Decline]int
	// Confirmations is how many destructive steps carried their approval each
	// way.
	Confirmations map[Confirmation]int

	// Reached is steps reached or correctly declined over non-optional steps
	// declared.
	Reached Ratio
	// AcceptedFirstTime is steps whose first call about them was the one that
	// reached them, over steps reached.
	AcceptedFirstTime Ratio
	// ArgumentFidelity is arguments whose value matched their truth over
	// arguments declared comparable. An authored value is in neither.
	ArgumentFidelity Ratio
	// Confirmation is destructive steps whose reaching call carried the
	// confirmation over destructive steps declared.
	Confirmation Ratio
	// Unaided is attempts completed with no refusal of ours anywhere over
	// attempts run.
	Unaided Ratio
	// Completion is attempts completed over attempts run.
	Completion Ratio
	// Clean is attempts that went right end to end with no help, over attempts
	// run. It is the headline figure and is a conjunction of the others rather
	// than an average of them: see [Verdict.Clean] for why there is no weighted
	// score here.
	Clean Ratio
	// Overhead is what the model spent getting there.
	Overhead Overhead
}

Totals is one row: the seven columns, the counts behind them, and what was left out of them.

func Aggregate

func Aggregate(verdicts []Verdict) Totals

Aggregate adds a set of verdicts into one row.

Five outcomes are counted and then left out of every column, each on its own ground. A skipped attempt never ran, so it is in no denominator, and a row that counted it would rank a model by the license of the instance it was measured on. An unobserved attempt ran and was not seen, so nothing about it can be said at all. The other three are left out because none of them is the model's: the provider would not answer, this side broke, or GitLab refused a call the model dispatched with the arguments the case declares. Counting any of those three would publish a provider's outage, a bug of ours or an instance's fixture as a model that did not complete the task, which is the fold section 4.2 refuses in so many words.

Their steps go with them. A step of such an attempt says as little as the attempt does: the work stopped where the outage or the refusal fell, so the steps after it were never reached for a reason that is not the model's, and counting them would move the charge from the completion column into the reached one rather than dropping it.

func Sum

func Sum(t, other Totals) Totals

Sum returns the two sets of totals as one.

Every figure a row publishes is a count or a ratio over attempts, and an attempt belongs to exactly one case, so totals taken over disjoint sets of cases add. That is what lets a published row be assembled from more than one run: a case re-measured on its own replaces its own contribution and the rest of the row keeps theirs, instead of the whole row being replaced by the one case somebody re-ran.

It is here rather than in the command that publishes rows because what is additive is a property of the scoring, not of the record: a figure that stopped being a sum of its cases would have to stop being added here, and a reader looking for that rule should find it beside the rule that computes it. TestSum_TouchesEveryField holds this to the struct by reflection, so a field added above and forgotten here fails rather than quietly reading low.

It is a function rather than a method because it mutates neither argument, and Totals already carries pointer-receiver methods that do: mixing the two receivers on one type is how a caller comes to believe a copy was updated.

type TruthKind

type TruthKind string

TruthKind names where an argument's right value came from.

const (
	// TruthFact is a value the recipe built and the stimulus was rendered with.
	TruthFact TruthKind = "fact"
	// TruthLiteral is a value the prompt states verbatim.
	TruthLiteral TruthKind = "literal"
	// TruthProduced is a field of an earlier step's answer.
	TruthProduced TruthKind = "produced"
	// TruthAuthored is a value the model writes, which is reported and never
	// compared.
	TruthAuthored TruthKind = "authored"
	// TruthNone is a truth that is none of the four. The corpus gate refuses
	// one, so it cannot arrive from a corpus in this tree; it is a kind of its
	// own so that a hand-written key in a test is reported rather than
	// silently compared against the empty string.
	TruthNone TruthKind = "none"
)

type Verdict

type Verdict struct {
	// Case is the corpus case identifier.
	Case string
	// Model is the provider specification that was asked.
	Model string
	// Surface, Mode and Tier are the row this attempt belongs to.
	Surface string
	Mode    string
	Tier    string
	// Session is the session label that served it.
	Session string
	// Repeat is which run of this attempt it was, from 1.
	Repeat int
	// Outcome is what became of it.
	Outcome Outcome
	// Reason says why, for every outcome but completed.
	Reason string
	// Steps are the declared steps and what became of each.
	Steps []StepVerdict
	// Discovery is how many catalog searches the model made. It is the
	// declared cost of the dynamic surface and is never a failure.
	Discovery int
	// InvalidParams is how many calls this server refused for their arguments,
	// which is a model learning a parameter name from a refusal. A refusal
	// that was itself a correct decline is not counted here: on the meta
	// surface a withheld action is refused by schema validation, and counting
	// that as a retry would charge a model for declining correctly.
	InvalidParams int
	// Unaided is whether the attempt completed with no refusal of ours
	// anywhere: no rejected arguments and no refused confirmation.
	Unaided bool
	// Verified is whether every recipe check against GitLab passed. An attempt
	// with no check recorded is verified by default, since there was nothing
	// to fail.
	Verified bool
}

Verdict is one attempt scored.

func Score

func Score(attempt Attempt, key modelcorpus.Key) (Verdict, error)

Score is the whole of this package's contract: one attempt's record and one case's key in, one verdict out.

The error is for a record this tree cannot read rather than for a model that did badly: a surface that is not one of the three, a tier the catalog cannot be built at, a catalog that will not build. A model that did badly is a verdict, not an error.

func ScoreCase

func ScoreCase(attempt Attempt) (Verdict, error)

ScoreCase is Score for a caller that may not hold a key: it resolves the answer for the case the attempt names and scores against that.

It exists for the runner. A run is meant to say what each attempt came to as it ends, and the corpus's boundary refuses a run that reads an answer, so the two statements could not both be true while the only entry point took a key. This package is one of the sanctioned readers, so the lookup happens here and what crosses back is a verdict about an attempt that has already ended: nothing a model was shown can be derived from it, and nothing written to the record comes from it either, since the record stays observation and a report re-scores it from the corpus at HEAD.

A case the corpus does not have is an error rather than an empty verdict. A zero verdict reads as a model that did nothing, which is a measurement, and an attempt naming a case nobody wrote is not one.

func (Verdict) Clean

func (v Verdict) Clean() bool

Clean reports whether this attempt went right end to end with no help.

It is the one figure that answers "does this model just work", and it is a conjunction rather than an average on purpose. The seven published columns measure different things over different denominators, and folding them into a weighted score would need weights nothing here can justify: there is no defensible reason to say argument fidelity is worth twice a confirmation or half of it, so the number would come from whoever chose the weights rather than from the measurement. Every term below is already measured, and an attempt has to satisfy all of them.

The terms are: the attempt completed; every non-optional step was reached or correctly declined; every argument the case gives a truth for matched; every destructive step carried its approval; nothing of ours had to refuse it; and what it did to GitLab checked out afterwards.

An unobserved step makes the answer false rather than true. A step whose dispatch the server never described cannot be said to have gone right, and the honest reading of "I could not see it" is not "it was fine".

Jump to

Keyboard shortcuts

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