rejectionparity

package
v1.15.4 Latest Latest
Warning

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

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

Documentation

Overview

Package rejectionparity owns the deliberate-rejection catalogue — the machine-readable inventory of every error-construction site in the three lowerings (internal/promql, internal/logql, internal/traceql) that can surface as an HTTP 422 "query is valid <QL> but cerberus rejects it" response.

Motivation: a deliberate rejection is a CLAIM about reference behaviour ("the reference backend cannot answer this either") that no other test layer verifies differentially. The `kind != nil` incident proved the failure mode: cerberus rejected a query reference Tempo accepts, and every test layer was blind to it because rejections were never diffed against the reference. This package makes that class of wrong belief impossible to pin silently:

  1. ScanSites enumerates every `fmt.Errorf("<head>: ...")` / `errors.New("<head>: ...")` construction site in the three lowering packages via go/ast — the mechanical universe of rejection candidates — plus every site built through a package-local `verbatimErrorf` wrapper (promql.verbatimErrorf is the one definition today), the deliberate, opt-in exception for a message that reproduces a reference backend's own wording and so carries no head prefix to scan for. See isErrorConstructor.
  2. The catalogue/ shard directory classifies every site into one of three classes — `rejection`, `internal`, or `divergence` (see below). It is stored as one shard per lowering SOURCE FILE (catalogue/internal__promql__subquery.go.json and friends), so two PRs fixing guards in different lowering files never write the same file and never blend into one another; LoadCatalogue merges the shards back into a single site-sorted value, and nothing downstream of it knows the artefact is sharded.
  3. The meta-tests in catalogue_test.go pin the three-way ratchet: scanned-site set == catalogue set (regenerable via CERBERUS_UPDATE_INVENTORY=1), every `rejection`/`divergence` entry's trigger query parses AND reaches an error matching the site's message — lowering itself for the overwhelming majority, or (an entry carrying GuardValues) lowering cleanly and applying the registered execution-time guard's Check, for the handful of sites reference Prometheus itself only rejects once a value exists to judge (see lowerGuardTrigger) — the parity-corpus case set is derived 1:1 from the `rejection` + `divergence` entries via BuildCases, and every entry's reachability evidence still matches what evidence.go derives from the lowering source today (see below).
  4. compatibility/cmd/rejection-parity consumes BuildCases inside each compat harness and, per case, asserts the class's claim: for `rejection`, that the REFERENCE backend also rejects the trigger query; for `divergence`, that it ACCEPTS it (see below). A `rejection` entry whose reference accepts is a wrong-rejection bug to fix at the source — never an allow-list entry.

Trigger queries must name metrics the lane's fixture seeds

Cerberus rejects at LOWERING time, before it reads a row, so on cerberus's side a trigger's verdict is purely shape-based and any metric name would do. The reference backends are not shape-based: PromQL's argument-domain checks (a double_exponential_smoothing smoothing factor outside (0, 1), say) live in the function BODY, which the engine never enters for a selector that matched no series. A trigger naming a metric the fixture does not seed therefore gets an empty 200 from the reference standing in for the 422 it would in fact have returned — the case reads as wrong_rejection while the two backends actually agree.

Two invariants follow, and both have bitten:

  • Trigger queries name fixture metrics (the promql lane seeds the `demo_*` family — see compatibility/prometheus/cmd/seed).
  • The driver queries INSIDE the seeded window. The promql lane's fixture is anchored at a fixed historical timestamp, so run-prometheus-compatibility.sh passes `-at "$END_TIME"`; the loki and tempo lanes seed relative to wall-clock and use the driver's default anchor.

The three classes

  • `rejection` — the parity claim is "cerberus and the reference backend both reject this query". Requires a trigger query (+ endpoint override where needed); forbids a rationale or a tracking issue.
  • `internal` — the site is not reachable from a parseable query through the HTTP query endpoints at all (parser-enforced shapes, internal invariants, %w error-propagation wrappers). Requires a rationale explaining why; forbids a trigger query, endpoint, or tracking issue.
  • `divergence` — the parity claim is "cerberus rejects this query AND the reference backend answers it, on purpose, and that gap is tracked". Requires everything `rejection` requires PLUS an open GitHub issue number (TrackingIssue) tracking closure.

Why `divergence` is not the allow-list this package forbids

The obvious wrong shape for a third class would be "cerberus knows it disagrees with the reference here, so stop checking" — exactly the allow-list mechanism note (4) above forbids, and exactly the failure mode that let the `kind != nil` incident (a real wrong-rejection bug) ship silently before this package existed. `divergence` is deliberately NOT that. It makes three POSITIVE, machine-checked claims, in both directions, every compat run:

  1. cerberus still rejects the trigger query — the same in-process reachability proof TestRejectionTriggersExerciseSites already runs for `rejection` entries, reused unchanged.
  2. the reference backend still accepts it — literally the inverse of what a `rejection` entry asserts; compatibility/cmd/ rejection-parity's runCase branches on the case's Class and checks the opposite status-class pairing.
  3. an issue in this repository, cited by number, is open and is an issue rather than a pull request — checked on every pull_request / push / merge_group by a dedicated forbid-deferral workflow step, mirroring the same liveness probe forbid-deferral.mjs already runs for deferral markers.

Each claim is a ratchet, not an exemption:

  • If someone fixes the divergence (cerberus starts answering), claim 1 fails — cerberus now returns 2xx for a case the catalogue says it rejects — and the entry must be deleted.
  • If upstream changes to also reject, claim 2 fails — both sides now return 4xx — and the entry must be reclassified to `rejection`.
  • If the tracking issue is closed while the entry remains, claim 3 fails regardless of what either backend does today.

A `divergence` entry can therefore never sit still: it is always either being actively re-verified against both backends, or it is failing the build. That is what distinguishes it from an allow-list entry, which by definition stops anyone from checking again.

Why that alone is not enough, and the two mechanisms that close the gap

The three claims above keep every EXISTING entry truthful, but they say nothing about the SET of entries: nothing stops it from growing without bound, and nothing stops an individual entry from sitting behind a permanently-open tracking issue forever. Two entries is a debt register; twenty is a parking lot wearing a better name — the allow-list dynamic this package forbids, arrived at from the other direction. divergence_ratchet.go adds the missing pressure with two independent mechanisms, both enforced by divergence_ratchet_test.go:

  1. A monotonic count ceiling (divergence-ceiling.json). The number of class=divergence entries may never increase without a hand-edited bump to that file in the SAME diff — CheckDivergenceCeiling fails the build otherwise. A decrease (an entry gets fixed or reclassified) is free and self-tightens: CERBERUS_UPDATE_INVENTORY=1 (the same regen convention TestCatalogueIsRegenerable uses) lowers the ceiling to match, but — mirroring test/coverage-floor.json's floor ratchet in the opposite direction — NextDivergenceCeiling refuses to RAISE it automatically. An increase is only ever a reviewable line a human wrote.
  2. A per-entry age cap (Entry.Since + divergenceStaleAfter). CheckDivergenceAge fails once an entry has sat open past the threshold, forcing a conscious re-decision instead of permanent silent tracking: fix the underlying rejection, close the tracking issue as won't-fix with the reasoning recorded there, or make a deliberate, acknowledged bump. This is expected to eventually fire on a divergence that is real, correct, and genuinely expensive to close — see #1768, where closing the label_replace duplicate-capture-group gap likely means moving regex expansion from ClickHouse's extractGroups to Go — and that is the point: the cap does not dispute the divergence, it only refuses to let it become permanent without someone looking at it again.

Why `internal` is not an allow-list either: the evidence block

`internal` is the largest class by a wide margin, and on its face it is the weakest: its whole content is a sentence of prose asserting that no wire query reaches the site. Prose does not decay when the code beneath it does. #1738 is the proof — a rationale claiming "every binary operator the parser produces maps to a chplan op" stayed green after the operator set it described had moved, because nothing re-read the switch it was describing. That is the exact shape of an allow-list: 161 sentences someone once believed, with no mechanism to notice when one stops being true.

Full static wire-reachability would be the ideal check — prove from the parsers' own grammars that no accepted input reaches the site — but it is not tractable here: it needs whole-program exhaustiveness analysis over three parsers' AST type sets, and roughly a third of the rationales rest on parse-level facts the lowering package cannot see at all. So the catalogue records REACHABILITY EVIDENCE instead, and re-derives it. Evidence is machine-extracted by evidence.go, per site, from the lowering source as it currently stands, in two stored fields chosen because each corresponds to a way the surveyed rationales actually argue:

  • Guard — the conditions that must hold for control to arrive at the site: the enclosing if/else chain, the switch arm, and — for `default` arms and for the trailing return after an exhaustive switch — the full inventory of SIBLING arms. That inventory is the load-bearing part: "every op is mapped" is a claim about the arms, so an arm that disappears moves the evidence.
  • Callers — the set of functions in the package that call the site's function. A rationale saying "only reached from X, which already checked" is falsified precisely by a second caller appearing, which is what happened in #1456.

Both are strictly local: the site's own function body and the direct callers of that function. Nothing wider is stored, and that boundary is deliberate. An earlier revision also stored the callers' own callees — the dispatch neighbourhood one hop up — which read well until an unrelated PR added one arm to a central dispatcher and moved the evidence of 28 sites in six untouched files. Demotion clears the class and the rationale, so a false positive costs a human re-statement; 28 of them at once makes blanket regeneration the only survivable answer, which is exactly the laundering this design exists to prevent. Evidence must be the facts a rationale DEPENDS ON, never a snapshot of its neighbourhood.

Rationales that argue from a SIBLING interception ("count_values is dispatched to lowerCountValues before buildAggFunc is consulted") are therefore held to account BY NAME. citedFunctions reads the prose, resolves each identifier it names against the lowering package's own declarations, and TestInternalRationaleCitationsStillDispatch fails when a cited function no longer exists or no longer has a caller. The cited set is derived from the rationale rather than from the code, so it depends on that one sibling and not on the other fourteen. It is recorded in the entry for review, and deliberately excluded from Equal and Diff: comparing it would mean that re-stating a rationale moved the evidence, and the demotion would erase the sentence just written. The gate is live instead — it re-derives citations on every run and cannot be regenerated past, because regeneration remembers a name that used to be cited until the prose itself is rewritten.

The gate is TestInternalRationalesRestOnCurrentEvidence: it re-derives evidence from the current source and diffs it against what the entry stores, failing with the site, its rationale, and the moved field. Every entry carries evidence, including `rejection` and `divergence` ones — no class is exempt from derivation, because an exempt class is how a tolerance file starts.

A fingerprint alone would still be waveable: regenerate, and the red goes away with nobody re-reading anything. So regeneration does not silently re-bless. When derived evidence differs from stored evidence for an `internal` entry, Generate DEMOTES it — clears both the class and the rationale — so the entry reappears as unclassified and TestCatalogueEntriesAreClassified fails until a human states the claim again against the code as it now stands. Deleting an evidence block by hand does not dodge this: absent evidence never equals derived evidence, so the demotion fires on the next regeneration. The two tests together mean an `internal` entry is either currently re-derivable or currently failing the build; it is never merely tolerated.

Adding a new rejection to a lowering therefore requires: a catalogue entry (the regen test fails without it), a trigger query (the exerciser test fails without it), and — by construction — a parity-corpus case the next compat run diffs against the reference backend. Reclassifying a `rejection` to `divergence` additionally requires filing (or citing) an open tracking issue first — the classification test fails without one. Classifying anything as `internal` requires a rationale that survives its own evidence being re-derived from the source on every run.

Index

Constants

View Source
const (
	EndpointPromInstant    = "promql_instant"
	EndpointLogQLRange     = "logql_range"
	EndpointTraceQLSearch  = "traceql_search"
	EndpointTraceQLMetrics = "traceql_metrics"
)

Endpoint identifiers consumed by compatibility/cmd/rejection-parity.

View Source
const (
	ClassRejection  = "rejection"
	ClassInternal   = "internal"
	ClassDivergence = "divergence"
)

Class identifiers — see the Entry.Class doc comment for the full semantics of each.

Variables

This section is empty.

Functions

func CheckDivergenceAge added in v1.15.0

func CheckDivergenceAge(e Entry, now time.Time) error

CheckDivergenceAge reports an error when e (class=divergence) has sat open longer than divergenceStaleAfter as of now, or when its Since date is unparseable. The message spells out the three legitimate remedies — fix it, close it won't-fix with recorded reasoning, or a deliberate, acknowledged ceiling bump — because the wrong remedy (quietly extending Since) is exactly the allow-list decay doc.go warns against. A divergence can be real, correct, and expensive to close (see #1768: ClickHouse's extractGroups cannot distinguish "group did not participate" from "participated but matched empty", the exact semantic Go's ExpandString relies on — closing it means moving the computation to Go); the cap does not dispute that, it only forces the decision to stay conscious.

func CheckDivergenceCeiling added in v1.15.0

func CheckDivergenceCeiling(count, ceiling int) error

CheckDivergenceCeiling reports an error when count exceeds ceiling. This is the enforcement half of the ratchet: it fires whether the excess came from a new divergence entry landing without a hand-edited ceiling bump, or (defensively) from a ceiling file update trying to raise the number on its own — NextDivergenceCeiling never returns a value this check would reject.

func CountDivergenceEntries added in v1.15.0

func CountDivergenceEntries(cat *Catalogue) int

CountDivergenceEntries counts the class=divergence entries in cat.

func DefaultEndpoint

func DefaultEndpoint(head string) string

DefaultEndpoint returns the per-head endpoint used when an entry does not override it.

func DiffShards added in v1.15.0

func DiffShards(dir string, want map[string][]byte) ([]string, error)

DiffShards compares the rendered shards in want against the files in dir, returning one human-readable line per difference (a shard that is missing, one that lingers with no entries left, or one whose bytes drifted). An empty result means the checked-in directory is byte-for-byte what regeneration would write.

func ErrorMatchesMessage

func ErrorMatchesMessage(errStr, format string) bool

ErrorMatchesMessage reports whether errStr contains every literal fragment of the format string, in order. This is the comparison the exerciser test uses to attribute a lowering error to a catalogue site without being brittle about the interpolated values.

func MarshalDivergenceCeiling added in v1.15.0

func MarshalDivergenceCeiling(c *DivergenceCeiling) ([]byte, error)

MarshalDivergenceCeiling renders the canonical on-disk JSON form (2-space indent + trailing newline), mirroring ShardCatalogue's per-shard rendering.

func MessageFragments

func MessageFragments(format string) []string

MessageFragments splits a fmt format string into its literal fragments — the chunks between %-verbs — trimmed of whitespace. Empty fragments are dropped.

func NextDivergenceCeiling added in v1.15.0

func NextDivergenceCeiling(count, existing int) (int, error)

NextDivergenceCeiling computes the ratcheted ceiling for the CERBERUS_UPDATE_INVENTORY regen path: it tightens the ceiling down to match count for free, but REFUSES to raise it — count > existing is returned as an error rather than a larger number, because an increase must be a hand-edited line a reviewer sees, never a tool's silent output (the same asymmetry coverage-summary.mjs's nextFloors enforces in the opposite direction for a floor).

func ShardCatalogue added in v1.15.0

func ShardCatalogue(cat *Catalogue) (map[string][]byte, error)

ShardCatalogue renders the canonical on-disk form: shard file name -> file bytes (2-space indent + trailing newline), entries partitioned by source file and sorted by site key inside each shard. A catalogue with no entries for a source file yields no shard for it, which is what makes pruning in WriteCatalogue a total operation rather than a guess.

func ValidEndpoint

func ValidEndpoint(head, ep string) bool

ValidEndpoint reports whether ep is a recognised endpoint for head.

func WriteCatalogue added in v1.15.0

func WriteCatalogue(dir string, cat *Catalogue) error

WriteCatalogue writes the sharded form into dir and REMOVES shards that carry no entries any more. Pruning is not housekeeping: a shard left behind after the last guard in its source file was deleted keeps feeding stale entries into LoadCatalogue, so the catalogue would go on asserting rejections that no longer exist while the regenerate-and-diff test — which only ever compared the files it wrote — reported green.

Types

type Case

type Case struct {
	// Name is the catalogue site key — stable, unique, and greppable
	// straight back to the error-construction site.
	Name string `json:"name"`
	Head string `json:"head"`
	// Endpoint is resolved (entry override or head default).
	Endpoint string `json:"endpoint"`
	Query    string `json:"query"`
	// Class is the entry's classification (ClassRejection or
	// ClassDivergence — BuildCases never emits ClassInternal cases).
	// The driver branches its verdict logic on this field: a
	// rejection case asserts BOTH backends reject; a divergence case
	// asserts cerberus rejects AND the reference backend answers.
	Class string `json:"class"`
}

Case is one parity-corpus case derived from a rejection or divergence entry. The driver sends Query to Endpoint on both backends; the expected verdict depends on Class — see compatibility/cmd/rejection-parity's runCase.

func BuildCases

func BuildCases(cat *Catalogue, head string) ([]Case, error)

BuildCases derives the parity corpus for one head from the catalogue: exactly one case per class=rejection or class=divergence entry, no more, no fewer. The 1:1 derivation is the "corpus-case count == catalogue count" leg of the ratchet — there is no separate corpus file to drift. Divergence entries are included deliberately: being checked on every compat run — with an inverted expected verdict — is the entire point of the class (see doc.go); silently dropping them from the corpus would turn "divergence" into exactly the allow-list the package forbids.

type Catalogue

type Catalogue struct {
	Entries []Entry `json:"entries"`
}

Catalogue is the merged, in-memory view of the checked-in artifact (the shard directory test/rejection-parity/catalogue/), sorted by site key. Its mechanical half is a go/ast scan of the fmt.Errorf/errors.New sites in internal/{promql,logql,traceql} (non-test files); the classification and trigger queries are curated by hand and pinned by this package's meta-tests.

Consumers see this one flat value regardless of how many shards it was assembled from — sharding is an on-disk concurrency measure, not a semantic split.

func Generate

func Generate(repoRoot string, prev *Catalogue) (*Catalogue, error)

Generate scans repoRoot and merges the result with the previous catalogue: sites present in prev keep their curated classification (class / trigger query / endpoint / rationale / tracking issue); new sites land with an empty class so the verify test demands curation; sites that disappeared from the source are dropped. Shrink and growth are both therefore deliberate, reviewable diffs. Evidence is never carried forward: it is rederived from the scan on every call, and a class=internal entry whose evidence MOVED loses its classification and its rationale (see reclassifyOnEvidenceDrift), so an unreachability claim can never outlive the source facts it was made about.

func LoadCatalogue

func LoadCatalogue(dir string) (*Catalogue, error)

LoadCatalogue reads every shard in dir and merges them into one catalogue sorted by site key — byte-for-byte the same in-memory value the single-file artifact used to produce, so nothing downstream of this function knows the artifact is sharded. A missing directory is returned as-is (os.IsNotExist holds) so the regen path can bootstrap from nothing.

type DivergenceCeiling added in v1.15.0

type DivergenceCeiling struct {
	MaxEntries int `json:"max_entries"`
}

DivergenceCeiling is the checked-in ratchet artifact (test/rejection-parity/divergence-ceiling.json): the maximum number of class=divergence entries the catalogue may carry.

func LoadDivergenceCeiling added in v1.15.0

func LoadDivergenceCeiling(path string) (*DivergenceCeiling, error)

LoadDivergenceCeiling reads + parses the checked-in ceiling file.

type Entry

type Entry struct {
	Site

	// Class is the curated classification:
	//
	//   "rejection"  — reachable from a parseable query: a deliberate
	//                  semantic rejection whose parity against the
	//                  reference backend the compat harnesses verify.
	//                  Requires TriggerQuery (+ Endpoint for traceql
	//                  metrics-pipeline sites); forbids Rationale and
	//                  TrackingIssue.
	//   "internal"   — not reachable from a parseable query through the
	//                  HTTP query endpoints: parser-enforced shapes,
	//                  internal invariants, error-propagation wrappers
	//                  (%w), or paths only reachable via non-wire entry
	//                  points. Requires Rationale; forbids TriggerQuery,
	//                  Endpoint and TrackingIssue.
	//   "divergence" — reachable AND wire-verified to differ from the
	//                  reference backend on purpose: cerberus rejects
	//                  the trigger query, the reference backend answers
	//                  it, and an open GitHub issue tracks closing the
	//                  gap. Requires TriggerQuery (+ Endpoint) exactly
	//                  like "rejection", plus TrackingIssue and Since;
	//                  forbids Rationale. See doc.go for why this is a
	//                  ratchet and not the allow-list the package
	//                  forbids, and for the two mechanisms (a count
	//                  ceiling + an age cap) that keep it that way.
	//
	// The verify test fails on any other value (including ""), so a
	// new rejection site cannot land unclassified.
	Class string `json:"class"`

	// TriggerQuery (class=rejection, class=divergence) is a minimal
	// concrete query that parses with the head's reference parser and
	// fails this site's lowering with this site's message. Pinned by
	// TestRejectionTriggersExerciseSites.
	TriggerQuery string `json:"trigger_query,omitempty"`

	// Endpoint (class=rejection, class=divergence) selects the HTTP
	// endpoint the parity driver sends TriggerQuery to. Empty means
	// the head default (DefaultEndpoint). TraceQL metrics-pipeline
	// rejections set "traceql_metrics" because /api/search does not
	// accept metrics expressions.
	Endpoint string `json:"endpoint,omitempty"`

	// GuardValues (class=rejection only, promql head only) is the
	// per-step value series fed directly to a registered
	// promql.ScalarGuard's Check when TriggerQuery alone cannot reach
	// the site: a lowering-time rejection is proved by lowering and
	// requiring an error (the TestRejectionTriggersExerciseSites
	// default), but an execution-time guard's Check only ever runs
	// against values ClickHouse would have produced, and some domain
	// branches (the int64-underflow arm of aggregationParamDomain) are
	// reachable only by a multi-step series a single lowering call can
	// never produce. When GuardValues is set, the exerciser instead
	// lowers TriggerQuery with a guard sink, requires lowering to
	// SUCCEED with exactly one registered guard, and applies that
	// guard's Check to GuardValues — mirroring the call shape
	// internal/engine/engine.go's runGuards uses in production, minus
	// the ClickHouse round trip.
	GuardValues []float64 `json:"guard_values,omitempty"`

	// Rationale (class=internal) documents why the site is not a
	// wire-reachable semantic rejection.
	Rationale string `json:"rationale,omitempty"`

	// TrackingIssue (class=divergence) is the number of an open GitHub
	// issue, in this repository, tracking closure of the divergence.
	// The forbid-deferral workflow asserts on every PR/push/merge_group
	// that the issue exists, is open, and is an issue rather than a
	// pull request — the same liveness contract forbid-deferral.mjs
	// already enforces for deferral markers. A closed or missing issue
	// with the entry still present is a failure: the entry must either
	// be deleted (cerberus was fixed) or re-filed against a fresh
	// issue, never left pointing at a closed one.
	TrackingIssue int `json:"tracking_issue,omitempty"`

	// Evidence is the machine-DERIVED reachability evidence for the
	// site: its guard chain and its intra-package reaching set, plus
	// the functions its own rationale names (Cited, which is derived
	// from the prose and never diffed). Generate recomputes it from the
	// go/ast scan on every regeneration and never carries the stored
	// value forward, so it is a fact about the current source rather
	// than a second declaration.
	//
	// It exists to hold class=internal Rationale claims to account.
	// A rationale is prose asserting that no wire query reaches the
	// site; that assertion rests on the guard the site sits behind and
	// on who can call the enclosing function — the two facts Guard and
	// Callers record, both strictly local to the site. When either
	// moves, Generate demotes the entry to unclassified and drops the
	// rationale (reclassifyOnEvidenceDrift), so the claim must be
	// re-made against the new source instead of being inherited. That
	// is the leg #1738 was missing: three lowerHoltWinters rationales
	// asserted a gate that no longer existed, and nothing re-derived
	// the assertion.
	//
	// Claims about a SIBLING interception ("count_values is dispatched
	// to lowerCountValues before buildAggFunc is consulted") are held to
	// account by name instead, through Cited and
	// TestInternalRationaleCitationsStillDispatch — not by storing the
	// caller's neighbourhood, which would make one new function in a
	// central dispatcher demote every rationale in the package.
	//
	// Evidence is recorded for EVERY entry, not just internal ones, so
	// that it is always already present when an entry is reclassified —
	// a field that only appears once a class is chosen would need a
	// bootstrap round-trip, and a bootstrap round-trip is a way to
	// launder a drifted rationale past the gate.
	Evidence *Evidence `json:"evidence,omitempty"`

	// Since (class=divergence) is the date, in divergenceDateLayout
	// ("2026-01-02"), the entry was first classified as a divergence —
	// backdated to the PR that introduced or reclassified it, never
	// reset by later edits. TestDivergenceEntriesRespectAgeCap fails
	// once now minus Since exceeds divergenceStaleAfter, so a
	// divergence cannot sit past its age cap silently: see
	// divergence_ratchet.go and doc.go.
	Since string `json:"since,omitempty"`
}

Entry is one catalogued site plus its curated classification.

type Evidence added in v1.15.0

type Evidence struct {
	// Guard is the chain of conditions, outermost first, that must hold
	// for the error construction to be reached inside its own function:
	// each enclosing `if` (or the `else` arm of one), and each `switch` /
	// type-switch case arm, rendered from the AST with whitespace
	// collapsed so reformatting alone never moves it. An empty chain
	// means the site is constructed unconditionally in its function's
	// body, in which case the whole unreachability claim rests on
	// Callers.
	Guard []string `json:"guard"`

	// Callers is the sorted set of functions declared in the SAME
	// lowering package that call the site's enclosing function. It is
	// the local reaching set: a new entry here is a new way to arrive at
	// the site, which is exactly the change #1738 recorded (a lowering
	// path reached a guard whose rationale claimed an earlier gate) and
	// exactly what a rationale about dispatch order stops being true
	// under.
	Callers []string `json:"callers"`

	// Cited is the lowering functions the entry's own Rationale names —
	// the gates it argues from ("intercepted by lowerCountValues
	// before..."). It is derived from the PROSE rather than from the
	// site, so it is deliberately excluded from Equal and Diff: it is not
	// a fact about the source that can drift, it is the list of names
	// TestInternalRationaleCitationsStillDispatch has to keep resolving.
	// Comparing it would also be circular — re-stating a rationale would
	// move it, and the demotion would then clear the sentence that had
	// just been written.
	Cited []string `json:"cited,omitempty"`
}

Evidence is the machine-DERIVED reachability evidence for one catalogued site: the facts about the current lowering source that a class=internal Rationale is a prose claim ABOUT. Nothing in it is hand-written — Generate recomputes every field from the go/ast scan on every regeneration, which is precisely what makes it evidence rather than a second declaration to drift alongside the first.

A rationale says "this site cannot be reached from the wire". Every such claim rests on two things and only two things: the conditions that must hold for the error to be constructed at all (Guard), and which functions can arrive at the one that constructs it (Callers). When either moves, the ground under the prose has moved with it, and Generate demotes the entry to unclassified so the claim has to be re-made by a human rather than silently inherited — see reclassifyOnEvidenceDrift.

Both fields are deliberately LOCAL to the site: the conditions inside its own function, and the direct callers of that function. Evidence must be the facts a rationale DEPENDS ON, never a snapshot of its neighbourhood — an earlier revision of this type also stored the union of everything those callers call, which meant adding one function to a central dispatcher demoted rationales package-wide. False demotions are not merely noisy: each one costs a human re-statement, so enough of them make blanket regeneration the only survivable response, which is exactly the laundering the demotion exists to prevent. What that field was reaching for — a rationale citing a specific sibling interception — is checked directly instead, by name, against the rationale that cites it: see unsupportedCitations.

func (*Evidence) Diff added in v1.15.0

func (e *Evidence) Diff(o *Evidence) []string

Diff renders the human-readable difference between the stored evidence and freshly derived evidence, one line per field that moved. It is what the drift gate prints, so the reader learns WHICH fact the rationale rested on stopped holding without opening the artifact.

func (*Evidence) Equal added in v1.15.0

func (e *Evidence) Equal(o *Evidence) bool

Equal reports whether two evidence values were derived from equivalent source. A nil receiver equals only another nil.

type Site

type Site struct {
	// Head is "promql" / "logql" / "traceql".
	Head string `json:"head"`
	// Site is the stable identifier
	// "internal/<head>/<file>.go:<func>#<hash8>[-<n>]" where <hash8>
	// is the first 8 hex chars of sha256(Message) and <n> is the
	// 1-based ordinal appended only when the same message is
	// constructed more than once inside the same function. Line
	// numbers are deliberately excluded so unrelated edits don't
	// churn the catalogue; hashing the message (rather than counting
	// positionally) keeps keys stable when an unrelated error site is
	// inserted earlier in the function.
	Site string `json:"site"`
	// Message is the raw format string of the error constructor —
	// verbs (%s / %d / %T / %w / ...) included. The exerciser test
	// matches lowering errors against the literal fragments between
	// the verbs (see MessageFragments / ErrorMatchesMessage).
	Message string `json:"message"`
}

Site is one error-construction site discovered by ScanSites: a fmt.Errorf / errors.New call in a lowering package whose format string starts with the head prefix.

func ScanSites

func ScanSites(repoRoot string) ([]Site, error)

ScanSites walks the three lowering packages under repoRoot and returns every prefixed error-construction site, sorted by site key. Test files and testdata are excluded — they construct errors for assertions, not for the wire.

Jump to

Keyboard shortcuts

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