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

autoupdate

package
v0.37.1 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 54 Imported by: 0

Documentation

Overview

Package autoupdate provides automated version checking and update application for ebuilds in the Bentoo overlay.

The package implements:

  • Package configuration management via TOML files
  • Version parsing from upstream sources (JSON, regex, LLM)
  • Cache management for version query results
  • Pending updates tracking and application. pending.json retains only items awaiting work (Pending) or post-mortem retry (Failed); a package that completes the full Apply success path is removed from pending.json so `--list` does not surface already-applied entries.

Configuration is read from overlay/.autoupdate/packages.toml which defines how to check upstream versions for each package. Local state is maintained in ~/.config/bentoo/autoupdate/ for caching and pending updates.

Response body size cap

Every HTTP response body is bounded so an oversized or malicious response cannot exhaust memory. Both GET helpers cap: RetryableHTTPClient.GetWithContext and RetryableHTTPClient.GetWithHeadersContext each wrap the response body in an http.MaxBytesReader capped at httpx.MaxBodyBytes (10 MiB); a read that exceeds the cap surfaces as an error wrapping ErrResponseTooLarge. The cap on the headers variant holds even for a Range request, since a server may ignore the Range header and stream the whole body.

The LLM clients (ClaudeClient, OpenAIClient, OllamaClient) apply the same 10 MiB default to their API response bodies, but the limit is per-client and can be raised with each client's WithMaxBodyBytes option because legitimate LLM responses (notably from a local Ollama instance) can exceed 10 MiB.

Usage:

checker, err := autoupdate.NewChecker(overlayPath)
if err != nil {
    log.Fatal(err)
}
batch := checker.CheckAll(false)
// batch.Items holds the successful CheckResults; batch.Failures maps a
// package name to the error that occurred; batch.ExitCode() yields the
// 0/1/2 process exit code.

Index

Constants

View Source
const (
	RequirementPresent = "present"
	RequirementPending = "pending"
	RequirementMissing = "missing"
)

The three values RequirementState.State takes.

View Source
const (
	// PriorityProvided is the highest priority for user-provided URLs
	PriorityProvided = 0
	// PriorityGitHub is the priority for GitHub releases API
	PriorityGitHub = 10
	// PriorityPyPI is the priority for PyPI API
	PriorityPyPI = 20
	// PriorityNPM is the priority for npm registry API
	PriorityNPM = 20
	// PriorityCrates is the priority for crates.io API
	PriorityCrates = 20
	// PriorityHomepage is the lowest priority for generic homepage scraping
	PriorityHomepage = 100
)

Priority constants for data source ordering

View Source
const (
	ContentTypeJSON = "application/json"
	ContentTypeHTML = "text/html"
)

Content type constants

View Source
const (
	ParserTypeJSON  = "json"
	ParserTypeHTML  = "html"
	ParserTypeRegex = "regex"
	ParserTypeLLM   = "llm"
)

ParserType constants for parser types

View Source
const (
	// ValidationSourceStaged means the gates were paid for by an earlier run and
	// this one promoted that run's tree.
	ValidationSourceStaged = "staged"
	// ValidationSourceThisRun means this run ran the gates itself.
	ValidationSourceThisRun = "this-run"
)

ValidationSource says where THIS apply's verdict came from, and it has exactly two values.

It is on the result rather than only in a log line because a fast green and a proved green look identical from the outside: an apply that took four seconds because the gates ran a year of Tuesdays ago and one that took four seconds because nothing was checked are the same four seconds. The operator is told which happened, per package.

View Source
const DefaultAnalysisCacheTTL = 24 * time.Hour

DefaultAnalysisCacheTTL is the default time-to-live for analysis cache entries (24 hours)

View Source
const DefaultApplyBuildConcurrency = 2

DefaultApplyBuildConcurrency bounds how many applies of one batch run their build gates at once. The rest of each apply — fetch, `pkgdev manifest`, staging — keeps the full --concurrency pool; only the build is narrowed, because concurrent builds contend for CPU and for space under PORTAGE_TMPDIR (60 MB measured for one gst configure).

View Source
const DefaultApplyLLMConcurrency = 2

DefaultApplyLLMConcurrency bounds how many LLM agents (manifest fixer, build fixer, bump reviewer) one Applier runs at once. Each agent is a long-lived claude child with its own token budget, so a pool of --concurrency of them would spend the budget and the provider's rate limit many times over.

View Source
const DefaultConcurrency = 10

DefaultConcurrency is the default number of packages processed in parallel when no explicit concurrency is configured. It governs both --check (CheckAll's per-package HTTP fan-out) and the --apply all worker pool. For --check, per-host rate limiting — not this number — bounds the request rate to any single provider (GitHub ~10/s, GitLab ~3/s), so the tuned limiters stay saturated. For --apply each worker runs a `pkgdev manifest` that fetches distfiles over the network and bypasses those limiters, so a moderate default keeps concurrent downloads from overwhelming a single host. 10 balances throughput against both.

View Source
const DefaultLLMTimeout = 60 * time.Second

DefaultLLMTimeout is the default per-operation timeout applied to a single LLM analysis call when no explicit timeout is configured on the Analyzer.

View Source
const DefaultOpTimeout = 30 * time.Second

DefaultOpTimeout is the default per-operation timeout applied to a single outbound HTTP fetch when no explicit timeout is configured on the Checker.

View Source
const DefaultPerHostConcurrency = 6

DefaultPerHostConcurrency caps the requests the checker has in flight to one host at a time. The rate limiter paces how often a request may START, not how many are still running: on a host that stops answering, every request it admits hangs for its whole timeout and retry budget, so a run with --concurrency 40 piles dozens of connections onto the one server that is already struggling. Six leaves GitHub — paced at ten starts a second — its throughput at normal latency.

View Source
const MaxPatternLen = 512

MaxPatternLen is the maximum allowed length, in characters, of an LLM-generated regex pattern. Patterns longer than this are rejected as a basic ReDoS prophylaxis.

View Source
const VendorGeneratedNote = "vendor tarball generated locally, not uploaded"

VendorGeneratedNote marks a check whose gates ran on a vendor tarball this run generated: a check never uploads one.

Variables

View Source
var (
	// ErrAnalysisCacheCorrupted is returned when the analysis cache file cannot be parsed
	ErrAnalysisCacheCorrupted = errors.New("analysis cache file is corrupted")
	// ErrAnalysisCacheMiss is returned when an analysis cache entry is not found or expired
	ErrAnalysisCacheMiss = errors.New("analysis cache miss")
)

Error variables for analysis cache errors

View Source
var (
	// ErrSchemaExists is returned when a schema already exists for a package
	ErrSchemaExists = errors.New("schema already exists for package")
	// ErrNoDataSources is returned when no data sources are found for a package
	ErrNoDataSources = errors.New("no data sources found for package")
	// ErrAnalysisFailed is returned when LLM analysis fails
	ErrAnalysisFailed = errors.New("LLM analysis failed")
	// ErrInvalidPattern is returned when an LLM-generated regex pattern is
	// invalid: it fails to compile, exceeds MaxPatternLen, or uses
	// backreferences (which RE2 does not support).
	ErrInvalidPattern = errors.New("invalid regex pattern")
)

Error variables for analyzer errors

View Source
var (
	// ErrManifestFailed is returned when the manifest command fails
	ErrManifestFailed = errors.New("manifest command failed")
	// ErrCompileFailed is returned when the compile test fails
	ErrCompileFailed = errors.New("compile test failed")
	// ErrNoPrivilegeEscalation is returned when neither sudo nor doas is available
	ErrNoPrivilegeEscalation = errors.New("no privilege escalation tool available (sudo or doas)")
	// ErrUserDeclined is returned when user declines the compile confirmation
	ErrUserDeclined = errors.New("user declined compile test")
	// ErrInvalidNewVersion is returned when the detected upstream version cannot
	// be coerced into a well-formed Gentoo PV (e.g. it carries a tag prefix that
	// survives normalization, or is not a version at all).
	ErrInvalidNewVersion = errors.New("invalid new version for ebuild")
	// ErrObsoletePending is returned (wrapped) when a pending entry no longer
	// matches the live overlay: the package was removed, or the overlay is
	// already at/beyond the target version. The entry is pruned and the outcome
	// is reported as obsolete, not as a failure.
	ErrObsoletePending = errors.New("obsolete pending entry")
	// ErrEbuildExists is returned when the destination ebuild a copy would write
	// is already present in the overlay. Overwriting it would destroy a file the
	// applier never authored — see copyEbuild's guard for why this is fatal
	// rather than a silent overwrite.
	ErrEbuildExists = errors.New("destination ebuild already exists")
	// ErrInvalidAuxValue is returned (wrapped) when a pending update's AuxValue
	// is outside auxValueRe. The value was scraped from an upstream page and is
	// written into the bash source of the new ebuild, so anything that could
	// close the quoted assignment or reach the shell is refused, not repaired.
	ErrInvalidAuxValue = errors.New("invalid aux value for ebuild")
	// ErrInvalidCommitHash is returned (wrapped) when a pending update's
	// CommitHash is not 40 lowercase hex — the only form substituteCommitHash
	// can find again on the next bump.
	ErrInvalidCommitHash = errors.New("invalid commit hash for ebuild")
	// ErrInvalidRequiredVersion is returned (wrapped) when a version captured
	// for a `requires` entry and replayed from pending.json is not a Gentoo
	// version as written. It is about to be written into bash, so the bump fails
	// before anything is staged or copied.
	ErrInvalidRequiredVersion = errors.New("invalid required version for ebuild")
	// ErrRequirementsNotCaptured marks a pending entry of a record that declares
	// `requires` but carries no captured version for one of them — an entry
	// queued before the record declared it, or by an older release. The bump
	// waits until `--check` captures the requirement.
	ErrRequirementsNotCaptured = errors.New("requirements not captured; re-run --check to capture them")
	// ErrRequirementPinNotFound is returned (wrapped) when the record declares a
	// pin the new ebuild does not carry: the registry and the ebuild disagree.
	ErrRequirementPinNotFound = errors.New("pinned requirement not found in ebuild")
	// ErrRepairPending is returned when the package's staged tree carries a
	// repair bundle and the run is not adopting that tree: restaging would
	// delete the operator's pending repair, so the package is refused instead.
	ErrRepairPending = errors.New("repair pending")
)

Error variables for applier errors

View Source
var (
	// ErrPackageNotInRegistry is returned when packages.toml holds no record
	// for the requested package. It is separate from ErrNoAuthFetch because the
	// remedies differ: one is a package this overlay does not track, the other
	// is a tracked package whose distfile needs no help.
	ErrPackageNotInRegistry = errors.New("no packages.toml record for this package")
	// ErrAmbiguousPackageKey is returned when the requested atom matches more
	// than one record — the slot and label suffixes exist precisely so one atom
	// can carry several entries, and guessing between them would fetch the
	// wrong release line in silence.
	ErrAmbiguousPackageKey = errors.New("the atom matches more than one packages.toml record")
	// ErrNoAuthFetch is returned for a record that configures no authenticated
	// download. Its distfile is on a mirror and `emerge` fetches it unaided, so
	// this is a "nothing to do here", not a failure of the download.
	ErrNoAuthFetch = errors.New("this package configures no authenticated distfile fetch")
)
View Source
var (
	// ErrFetchFailed is returned when fetching upstream version fails
	ErrFetchFailed = errors.New("failed to fetch upstream version")
	// ErrUpstreamUnreachable is wrapped beside ErrFetchFailed when the fetch
	// failed in transport — retries exhausted, a timeout, an open circuit
	// breaker — rather than on what the record asked for. Nothing in the record
	// is wrong, so the interactive registry repair must not offer to rewrite it.
	ErrUpstreamUnreachable = errors.New("upstream unreachable")
	// ErrRequirementUnresolved is returned (wrapped) when a record's `requires`
	// entry could not be captured for the detected version. The bump is held,
	// like an unresolved aux value: a pending entry with a partial requirement
	// set could not be told apart from a complete one.
	ErrRequirementUnresolved = errors.New("requirement unresolved")
	// ErrBaseVersionUnresolved is returned when an entry declares where its base
	// version lives (base_from, or commit_version_pattern) and that source yields
	// nothing. It is deliberately fatal for the check: falling back to the
	// ebuild's own base is what let six Khronos packages drift up to seven
	// releases behind while still reporting "up to date".
	ErrBaseVersionUnresolved = errors.New("declared base version source resolved nothing")
	// ErrAuxUnresolved is recorded (wrapped, on CheckResult.Error) when a package
	// declares aux_pattern or commit_sha_path and that value could not be
	// resolved. The bump is held rather than queued: applying it would ship the
	// previous release's value under the new version.
	ErrAuxUnresolved = errors.New("auxiliary value unresolved")
)

Error variables for checker errors

View Source
var (
	// ErrPendingCorrupted is returned when the pending file cannot be parsed
	ErrPendingCorrupted = errors.New("pending file is corrupted")
	// ErrPackageNotInPending is returned when a package is not found in pending updates
	ErrPackageNotInPending = errors.New("package not found in pending updates")
	// ErrInvalidStatusTransition is returned when an invalid status transition is attempted
	ErrInvalidStatusTransition = errors.New("invalid status transition")
)

Error variables for pending list errors

View Source
var (
	// ErrVendorGenerate marks a Go vendor tarball that could not be generated,
	// or a served one that could not be compared with a regenerated tree.
	ErrVendorGenerate = errors.New("vendor tarball generation failed")
	// ErrVendorStale marks a served vendor tarball whose name does not carry
	// the candidate's version and whose vendor/modules.txt no longer matches
	// the one the candidate's source produces.
	ErrVendorStale = errors.New("pinned vendor tarball is stale")
)
View Source
var (
	// ErrVendorUpload marks a generated vendor tarball whose publication was not
	// proved: the upload command failed or timed out, the served bytes differ
	// from the generated ones, or the tarball changed before it was published.
	ErrVendorUpload = errors.New("vendor tarball upload failed")
	// ErrVendorUploadPending marks a generated vendor tarball that was kept
	// rather than published because no upload command is configured. Its
	// wrapped text names the kept path and the URL it must be served from.
	ErrVendorUploadPending = errors.New("vendor tarball upload pending")
)
View Source
var ErrBuildEnvironment = errors.New("the build failed on the environment, not on the ebuild")

ErrBuildEnvironment reports that a failed build failed on the machine rather than on the ebuild: the host is missing build dependencies, the run died before the ebuild's own phases began, the build device ran out of space, or Portage's build directory is not writable.

It is the sentinel the build-fix gate keys on, exactly as ErrManifestEnvironment is for the manifest step. An agent handed such a failure has one repair available to it — edit the ebuild — and the ebuild is not what failed, so it spends turns and money concluding that a correct file is wrong. Callers test this with errors.Is; the reason and the build's own error are always wrapped alongside, so the operator still reads what actually happened.

View Source
var ErrInvalidSweepTarget = errors.New("invalid sweep target")

ErrInvalidSweepTarget is returned for a target that is neither a category nor a "category/package" atom present in the overlay.

View Source
var ErrManifestEnvironment = errors.New("manifest step failed on the environment, not on the ebuild")

ErrManifestEnvironment reports that a failed manifest step failed on the machine, not on the ebuild: the distdir stopped being writable, its filesystem ran out of room, or the fetch left an empty artefact behind.

It is what the fixer gate keys on. An LLM fixer handed such a failure has no repair available to it — its only reachable conclusion is that SRC_URI is wrong — so it edits a correct ebuild in a repository that commits and pushes on its own. Callers test this with errors.Is; the original error is always wrapped alongside it, so the operator still reads exactly what pkgdev said.

View Source
var ErrManifestIncomplete = errors.New("manifest is missing DIST entries")

ErrManifestIncomplete is returned (wrapped) when the manifest step succeeded but the new ebuild's SRC_URI names a distfile its Manifest has no DIST line for. Such an ebuild passes every later gate that does not fetch, and fails on the user's machine with "does not have a Manifest entry" — a batch bump once shipped 76 of them.

View Source
var ErrNoLocalPackageDir = errors.New("the gentoo provider has no local package directory")

ErrNoLocalPackageDir reports that the ::gentoo provider exposes no on-disk package directory (it is not a provider.PackageDirProvider), so a revive has no base ebuild to seed from.

View Source
var ErrScriptSupportNotBuilt = errors.New(
	"script parser support not built: rebuild with -tags chromedp; " +
		"a Chrome or Chromium executable is required at run time")

ErrScriptSupportNotBuilt is returned when a parser="script" package is checked by a binary compiled WITHOUT the chromedp headless-browser backend. The real evaluator lives behind the `chromedp` build tag and drives a Chrome or Chromium already installed on the system; the default build ships only the testable interface so the browser dependency stays opt-in.

Functions

func ApplyFallbackToSchema

func ApplyFallbackToSchema(schema *registry.PackageConfig, fallback *FallbackSuggestion)

ApplyFallbackToSchema applies a fallback suggestion to a PackageConfig. It sets the FallbackParser field based on the suggestion.

func CanRevive

func CanRevive(prov provider.Provider) error

CanRevive reports, before any other work, whether prov can seed a revive. It returns a wrapped ErrNoLocalPackageDir when prov is not a PackageDirProvider.

func ClassifyManifestFailure

func ClassifyManifestFailure(distdir string, err error, expected []string, space SpaceFunc) error

ClassifyManifestFailure decides whether err — the error a failed `pkgdev manifest` returned — is an environment failure or a repairable one, by checking observable state in this order (an unwritable distdir explains an empty artefact, so the cause is named before its symptom):

  1. distdir no longer writable (the pre-flight probe, repeated) -> environment
  2. free space below the floor any fetch needs -> environment
  3. an expected artefact present with zero length -> environment
  4. none of the above -> repairable

For 1-3 it returns an error wrapping both ErrManifestEnvironment and err, so the operator still reads what pkgdev printed. For 4 it returns err untouched, the same value, so a caller testing it for something else keeps finding it.

"Uncertain" means "repairable": a check that cannot be answered (an empty distdir, a failing space query, an uninspectable name) is no evidence and falls through. A nil space uses the real statfs query rather than skipping. expected holds untrusted distfile NAMES, each reduced to one filename before the join. It never panics, never logs, and writes only Probe's own file.

func EnhanceSchemaWithFallback

func EnhanceSchemaWithFallback(schema *registry.PackageConfig)

EnhanceSchemaWithFallback adds fallback configuration to a schema based on its primary parser. This is called after LLM analysis to ensure fallback is always configured.

func IsFallbackOrderValid

func IsFallbackOrderValid(fallbacks []FallbackSuggestion) bool

IsFallbackOrderValid checks if fallbacks are ordered by reliability. Returns true if fallbacks are in correct order (JSON > HTML > regex > LLM).

func IsValidStatus

func IsValidStatus(s UpdateStatus) bool

IsValidStatus checks if a status is valid

func PackageAtom

func PackageAtom(key string) string

PackageAtom returns the "category/package" a package key names, dropping any ":slot" and "@label", and "" for a key that is not an atom. The --apply all planner uses it to match a pending entry against another entry's requirement.

func RepairableFetchFailures

func RepairableFetchFailures(failures map[string]error) []string

RepairableFetchFailures returns, in lexical order, the packages whose failure wraps ErrFetchFailed — the only class the registry fixer can repair. The match is errors.Is, never the error text: a failure that merely says "failed to fetch upstream version" is not repairable, and one wrapped twice or joined with another error still is.

A failure that also wraps ErrUpstreamUnreachable is excluded: a timeout or a TLS EOF is a fetch failure the record did not cause, so offering to rewrite the record would invite a distracted "y" to break an entry that was correct.

func RequiresBuildSlot added in v0.36.0

func RequiresBuildSlot(d validate.Depth) bool

RequiresBuildSlot reports whether an apply validating at depth d must hold one of the Applier's build slots while its build gates run.

Every depth that STARTS A BUILD does: concurrent builds contend for CPU and for space under PORTAGE_TMPDIR, measured at 60 MB for one gst configure, so they are bounded below the --concurrency pool (DefaultApplyBuildConcurrency). Depths none and options only read files that are already on disk and take no slot.

func StalePinBatch

func StalePinBatch(divs []Divergence) map[string]string

StalePinBatch builds the write batch, and is the ONLY place a Divergence becomes something written to the registry.

It switches on Kind because the three classes are not interchangeable, and mapping all of them to Key -> Disk corrupts the registry in two distinct ways:

  • NoEbuild carries an empty Disk, so writing it would ERASE the entry's pin — and an entry with no pin blocks its whole directory's next --clean;
  • UnclaimedEbuild's Key is a bare "category/package" atom, not a registry key, and the two routinely coincide (net-misc/rclone is both). Writing it would point that entry's pin at the stray ebuild the finding is asking a human to delete: the exact opposite of the intent.

A future fourth kind falls through unwritten, which is the safe direction.

func ValidateFallbackChain

func ValidateFallbackChain(schema *registry.PackageConfig) error

ValidateFallbackChain validates that a schema's fallback configuration is valid. It checks that the fallback parser is different from the primary parser and that required fields are set.

func VendorUploadAction added in v0.36.0

func VendorUploadAction(err error) (action string, ok bool)

VendorUploadAction returns the "upload <path> to <uri>" action an ErrVendorUploadPending error carries; ok is false for any other error. The error text is the single source of the kept path and the URL, so every renderer reads them from here.

func VersionSatisfies

func VersionSatisfies(pin, have, want string) bool

VersionSatisfies reports whether an ebuild at version have meets the pin `<pin><category>/<package>-<want>`:

  • "~" — have, with its -rN revision dropped, equals want;
  • "=" — have equals want exactly, revision included;
  • ">=" — have compares greater than or equal to want.

Any other pin is never satisfied. It is the one implementation of the rule: the presence scan, the check report's pending state and the --apply all wave planner all call it, so the three can never disagree about a revision.

Types

type AnalysisCache

type AnalysisCache struct {
	// Entries holds all cached analysis entries, keyed by package name
	Entries map[string]AnalysisCacheEntry `json:"entries"`
	// TTL is the time-to-live for cache entries (default: 24 hours)
	TTL time.Duration
	// contains filtered or unexported fields
}

AnalysisCache manages LLM analysis result caching with TTL-based expiration. It persists cache entries to disk and supports concurrent access. Cache is stored in ~/.config/bentoo/autoupdate/analysis_cache.json

func NewAnalysisCache

func NewAnalysisCache(configDir string, opts ...AnalysisCacheOption) (*AnalysisCache, error)

NewAnalysisCache creates or loads an analysis cache from disk. If the cache file exists, it loads existing entries. If the cache file doesn't exist or is corrupted, it creates a new empty cache. The configDir should be the bentoo config directory (e.g., ~/.config/bentoo/autoupdate).

func (*AnalysisCache) Cleanup

func (c *AnalysisCache) Cleanup() error

Cleanup removes all expired entries from the analysis cache. It automatically saves the cache to disk after cleanup.

func (*AnalysisCache) Clear

func (c *AnalysisCache) Clear() error

Clear removes all entries from the analysis cache. It automatically saves the cache to disk after clearing.

func (*AnalysisCache) Delete

func (c *AnalysisCache) Delete(pkg string) error

Delete removes a package from the analysis cache. It automatically saves the cache to disk after deletion.

func (*AnalysisCache) Get

Get retrieves a cached schema if it exists and is not expired. Returns the schema and true if found and valid, nil and false otherwise.

Get also performs lazy revalidation: a cached schema whose regex pattern or XPath expression no longer passes validation (e.g. it was written by an older, less strict build) is evicted via Delete and reported as a cache miss. A miss — not an error — is the normal signal that prompts the caller to re-run analysis.

func (*AnalysisCache) GetEntry

func (c *AnalysisCache) GetEntry(pkg string) (AnalysisCacheEntry, bool)

GetEntry retrieves the full analysis cache entry for a package. Returns the entry and true if found, zero value and false otherwise. This does not check TTL - use Get for TTL-aware retrieval.

func (*AnalysisCache) GetWithBypass

func (c *AnalysisCache) GetWithBypass(pkg string, bypass bool) (*registry.PackageConfig, bool)

GetWithBypass retrieves a cached schema, optionally bypassing the cache. If bypass is true (--no-cache flag), always returns cache miss. Returns the schema and true if found and valid (and not bypassed), nil and false otherwise.

func (*AnalysisCache) Len

func (c *AnalysisCache) Len() int

Len returns the number of entries in the analysis cache.

func (*AnalysisCache) Save

func (c *AnalysisCache) Save() error

Save persists the analysis cache to disk. This is thread-safe and can be called concurrently.

func (*AnalysisCache) Set

func (c *AnalysisCache) Set(pkg string, schema *registry.PackageConfig, url string) error

Set stores a schema in the analysis cache with the current timestamp. It automatically saves the cache to disk after setting.

type AnalysisCacheEntry

type AnalysisCacheEntry struct {
	// Schema is the cached PackageConfig generated by LLM analysis
	Schema *registry.PackageConfig `json:"schema"`
	// Timestamp is when this entry was cached
	Timestamp time.Time `json:"timestamp"`
	// URL is the URL that was analyzed to generate this schema
	URL string `json:"url"`
}

AnalysisCacheEntry represents a cached LLM analysis result. It stores the generated schema, when it was cached, and the source URL.

type AnalysisCacheOption

type AnalysisCacheOption func(*AnalysisCache)

AnalysisCacheOption is a functional option for configuring AnalysisCache

func WithAnalysisCacheLogger

func WithAnalysisCacheLogger(l *slog.Logger) AnalysisCacheOption

WithAnalysisCacheLogger sets the logger the cache reports its diagnostics to. Nil keeps the default, which discards them.

func WithAnalysisCacheNowFunc

func WithAnalysisCacheNowFunc(fn func() time.Time) AnalysisCacheOption

WithAnalysisCacheNowFunc sets a custom time function for testing

func WithAnalysisCacheTTL

func WithAnalysisCacheTTL(ttl time.Duration) AnalysisCacheOption

WithAnalysisCacheTTL sets a custom TTL for the analysis cache

type AnalyzeOptions

type AnalyzeOptions struct {
	// URL overrides the URL for analysis
	URL string
	// Hint provides user guidance to the LLM
	Hint string
	// NoCache bypasses all caches
	NoCache bool
	// Force overwrites existing schema
	Force bool
	// DryRun shows schema without saving
	DryRun bool
}

AnalyzeOptions configures the analysis behavior.

type AnalyzeResult

type AnalyzeResult struct {
	// Package is the full package name (category/package)
	Package string
	// SuggestedSchema is the schema suggested by analysis
	SuggestedSchema *registry.PackageConfig
	// Validated indicates if the schema was validated successfully
	Validated bool
	// ExtractedVersion is the version extracted using the schema
	ExtractedVersion string
	// EbuildVersion is the current version from the ebuild
	EbuildVersion string
	// Error contains any error that occurred during analysis
	Error error
	// DataSource is the data source used for analysis
	DataSource *DataSource
	// FromCache indicates if the result was from cache
	FromCache bool
}

AnalyzeResult represents the result of analyzing a package.

type Analyzer

type Analyzer struct {
	// contains filtered or unexported fields
}

Analyzer handles package analysis and schema generation. It coordinates between ebuild metadata extraction, data source discovery, LLM analysis, and schema validation.

func NewAnalyzer

func NewAnalyzer(overlayPath string, opts ...AnalyzerOption) (*Analyzer, error)

NewAnalyzer creates a new analyzer instance for the given overlay.

func (*Analyzer) Analyze

func (a *Analyzer) Analyze(ctx context.Context, pkg string, opts AnalyzeOptions) (*AnalyzeResult, error)

Analyze analyzes a single package and suggests a schema. Every fetch and LLM call it makes is bounded by a child of ctx, so cancelling ctx (e.g. on SIGINT) aborts the analysis in flight.

func (*Analyzer) AnalyzeAll

func (a *Analyzer) AnalyzeAll(ctx context.Context, opts AnalyzeOptions) BatchResult[AnalyzeResult]

AnalyzeAll analyzes all packages without schemas, at most 3 at a time.

It returns a BatchResult: analyzed packages land in Items, a per-package failure is recorded in Failures keyed by package name and the batch goes on. A failure to enumerate the packages is a single synthetic Failures entry, which yields a total-failure exit code.

The pool follows CheckAll's model:

  • a slot is taken BEFORE a worker starts, so at most 3 analyses run at once and a package waiting for its turn holds no goroutine;
  • once ctx is done, no package without a slot is started: each is recorded in Failures wrapping the context's error, and running analyses finish;
  • a panic in one analysis is recovered as that package's failure ("panic: <value>"), so it neither stops the others nor crashes the process;
  • Items are sorted by Package, independent of which analysis finished first.

Every write to the shared BatchResult is mutex-guarded, and it is returned only after every worker goroutine has joined (wg.Wait), so callers may safely invoke its methods (ExitCode, FormatFailures) on the returned value.

func (*Analyzer) Cache

func (a *Analyzer) Cache() *AnalysisCache

Cache returns the analysis cache.

func (*Analyzer) Config

func (a *Analyzer) Config() *registry.PackagesConfig

Config returns the packages configuration.

func (*Analyzer) FetchContent

func (a *Analyzer) FetchContent(ctx context.Context, source DataSource) ([]byte, string, error)

FetchContent fetches content from a data source (exported for testing). The fetch is bounded by ctx.

func (*Analyzer) LoadAndMergeSchema

func (a *Analyzer) LoadAndMergeSchema(pkg string, schema *registry.PackageConfig) error

LoadAndMergeSchema loads existing config, adds/updates a schema, and saves. This ensures existing entries are preserved when adding new schemas.

func (*Analyzer) OverlayPath

func (a *Analyzer) OverlayPath() string

OverlayPath returns the overlay path.

func (*Analyzer) SaveSchema

func (a *Analyzer) SaveSchema(pkg string, schema *registry.PackageConfig) error

SaveSchema saves a validated schema to packages.toml.

type AnalyzerOption

type AnalyzerOption func(*Analyzer) error

AnalyzerOption is a functional option for configuring Analyzer.

func WithAnalyzerCache

func WithAnalyzerCache(cache *AnalysisCache) AnalyzerOption

WithAnalyzerCache sets a custom analysis cache for the analyzer.

func WithAnalyzerConfigDir

func WithAnalyzerConfigDir(dir string) AnalyzerOption

WithAnalyzerConfigDir sets the configuration directory for the analyzer.

func WithAnalyzerHTTPClient

func WithAnalyzerHTTPClient(client *fetch.RetryableHTTPClient) AnalyzerOption

WithAnalyzerHTTPClient sets a custom HTTP client for the analyzer.

func WithAnalyzerLLMClient

func WithAnalyzerLLMClient(llm llm.LLMProvider) AnalyzerOption

WithAnalyzerLLMClient sets a custom LLM client for the analyzer.

func WithAnalyzerLLMTimeout

func WithAnalyzerLLMTimeout(d time.Duration) AnalyzerOption

WithAnalyzerLLMTimeout sets the per-operation timeout used to derive a child context for each LLM analysis call. A non-positive duration is rejected.

func WithAnalyzerLogger

func WithAnalyzerLogger(l *slog.Logger) AnalyzerOption

WithAnalyzerLogger sets the logger the analyzer reports its diagnostics to, and hands it to the analysis cache and HTTP client the analyzer uses. Nil keeps the default, which discards them (and leaves an injected cache's or client's own logger alone).

func WithAnalyzerOpTimeout

func WithAnalyzerOpTimeout(d time.Duration) AnalyzerOption

WithAnalyzerOpTimeout sets the per-operation timeout used to derive a child context for each outbound HTTP fetch. A non-positive duration is rejected.

func WithAnalyzerPackagesConfig

func WithAnalyzerPackagesConfig(config *registry.PackagesConfig) AnalyzerOption

WithAnalyzerPackagesConfig sets a custom packages configuration.

func WithAnalyzerRateLimiter

func WithAnalyzerRateLimiter(limiter *fetch.RateLimiter) AnalyzerOption

WithAnalyzerRateLimiter sets a custom rate limiter for the analyzer.

type Applier

type Applier struct {
	// contains filtered or unexported fields
}

Applier handles update application for packages. It coordinates between pending list and file system operations.

func NewApplier

func NewApplier(overlayPath, configDir string, opts ...ApplierOption) (*Applier, error)

NewApplier creates a new applier instance for the given overlay. It initializes the pending list and logs directory.

func (*Applier) Apply

func (a *Applier) Apply(ctx context.Context, pkg string, compile bool) (result *ApplyResult, _ error)

Apply applies a pending update for a package. It copies the ebuild to the new version and runs the manifest command. If compile is true, it also runs a compile test with elevated privileges.

The result is returned via a named value so a single deferred cleanup can observe whichever error the function ultimately surfaces (result.Error is kept in lockstep with the returned error on every path).

func (*Applier) ApplyAll

func (a *Applier) ApplyAll(ctx context.Context, updates []PendingUpdate, compile bool, concurrency int) ([]*ApplyResult, int)

ApplyAll applies every pending update through the shared Applier and returns the per-package results in input order plus the number of hard failures (an Apply returning a non-nil error). It is the concurrency seam of `bentoo overlay autoupdate --apply all` (cmd/bentoo runApplyAll).

Every batch runs on a worker pool of concurrency workers, clamped to [1, len(updates)], whatever the flags or the validation depths. The expensive stages are bounded inside Apply instead (apply_slots.go): build gates by build slots, the privileged --compile step one at a time so its prompt and sudo invocation never interleave, and the LLM agents by LLM slots. The pool is safe because the Applier's pending list and reporter are mutex-guarded, each Apply works in its own package directory, and workers write distinct result indices.

Every Apply receives ctx and returns a failed result at once on a done ctx, so after a cancel each unstarted package still gets a non-nil, failed result in input order; an early break would leave nil results instead. A bump that requires another pending bump runs in a later wave (applyWaves), so its requirement gate sees what the earlier one published.

func (*Applier) EbuildPath

func (a *Applier) EbuildPath(pkg, version string) string

EbuildPath returns the full path to an ebuild file in the PUBLISHED overlay, or "" when pkg is not a well-formed category/package. It delegates so that the filename an ebuild carries is built in exactly one place (candidateIn).

func (*Applier) HTTPClient added in v0.36.0

func (a *Applier) HTTPClient() *fetch.RetryableHTTPClient

HTTPClient returns the client the vendor step probes and downloads with; nil means the vendor step is off.

func (*Applier) LogsDir

func (a *Applier) LogsDir() string

LogsDir returns the logs directory path.

func (*Applier) MarkReenabled

func (a *Applier) MarkReenabled(pkg string)

MarkReenabled tells the Applier that pkg's entry was re-enabled in packages.toml after the Applier loaded it, so its stale enabled = false no longer refuses the bump. hold = true still does. It needs an Applier built by NewApplier, which allocates the set.

func (*Applier) OverlayPath

func (a *Applier) OverlayPath() string

OverlayPath returns the overlay path.

func (*Applier) Pending

func (a *Applier) Pending() *PendingList

Pending returns the pending list instance.

func (*Applier) RequirePin

func (a *Applier) RequirePin(pkg, atom string) (string, bool)

RequirePin returns the pin operator pkg's record declares for atom, and false when the record does not (any more) require it.

func (*Applier) SeedFromGentoo

func (a *Applier) SeedFromGentoo(pkg, srcPkgDir, gentooVersion string) error

SeedFromGentoo copies the current ::gentoo package directory (srcPkgDir) into the overlay so a previously-removed (orphaned) package has a base ebuild to revive from. The overlay package dir may not exist yet — it is created with os.MkdirAll. Only the parts needed to bootstrap a bump are copied:

  • the single ebuild matching gentooVersion (required; an absent source ebuild is a clear error),
  • metadata.xml (optional; skipped silently when absent),
  • the files/ subdirectory (optional; copied recursively, preserving the relative layout).

The Manifest is deliberately NOT copied: it is regenerated by the later bump's `pkgdev manifest` step against the overlay's own distfiles. Every failure is wrapped with %w.

func (*Applier) StagingRoot

func (a *Applier) StagingRoot() string

StagingRoot returns the directory this applier stages candidates under, empty when it was constructed without one (in which case no gate runs and the candidate is written straight into the published overlay).

It is exported because the staging root is where the EVIDENCE lives: the retained tree of every bump that was not promoted and the record beside it. A caller that has to tell an operator where to look — or that wants to prove that a promotion really came from a tree an earlier run left there — cannot do either from the option it passed in, because the applier is the thing that decides what to do with it.

func (*Applier) Validate

func (a *Applier) Validate(ctx context.Context, pkg string, ceiling validate.Depth) validate.EbuildResult

Validate runs one pending update through the gates at its resolved depth and reports what each gate said, without publishing anything.

ceiling is the depth the operator confirmed for the whole run. A reviewer's escalation is held there rather than spent unasked: a plan that resolved entirely to `options` asked the operator about nothing, so a bump raised to `compile` afterwards would spend hours they never saw priced. DepthNone means "no ceiling", which is what a caller that did not ask anybody passes.

IT NEVER RETURNS AN ERROR, and that is deliberate. A check evaluates a BATCH: one package whose tree could not be staged, or whose pending entry went stale between the check and here, must not stop the other three hundred. Every such case comes back as a SKIPPED gate carrying the reason, which is what EbuildResult.WorstOutcome reads as "nothing was said about this bump" — never as a pass.

func (*Applier) VendorConfig added in v0.36.0

func (a *Applier) VendorConfig() config.VendorConfig

VendorConfig returns the autoupdate.vendor block this applier was built with.

type ApplierOption

type ApplierOption func(*Applier)

ApplierOption is a functional option for configuring Applier

func WithAdoptedStagedTree added in v0.36.0

func WithAdoptedStagedTree(root string) ApplierOption

WithAdoptedStagedTree makes Apply adopt the staged tree at root, kept by an earlier failed apply and edited since, instead of staging a fresh candidate: the manifest step, the gates and promotion run on that tree's bytes exactly as they stand. No LLM agent runs on an adopted tree — the manifest fixer, the build fixer and the bump reviewer are left out of the run, whatever was wired — because its candidate is the operator's work, not an agent's input. The tree is never removed by the run; a failure keeps it and rewrites its repair bundle. An empty root leaves the applier as it was.

func WithApplierBuildConcurrency added in v0.36.0

func WithApplierBuildConcurrency(n int) ApplierOption

WithApplierBuildConcurrency sets how many applies may run their build gates at once (default DefaultApplyBuildConcurrency; values below 1 mean 1).

func WithApplierBuildFixer

func WithApplierBuildFixer(fixer fixer.BuildFixer) ApplierOption

WithApplierBuildFixer wires an LLM build fixer into the applier. When the build gate fails for a reason attributable to the ebuild, the applier asks the fixer to repair the STAGED ebuild and then re-runs the same gate to decide. A nil fixer is ignored, preserving the fail-fast behaviour.

The nil discipline is WithApplierFixer's, deliberately and to the letter: a provider that could not be constructed leaves the applier exactly as it was rather than half-configured, so "no LLM was asked for" and "the LLM could not be built" produce the same, predictable run.

func WithApplierBumpReviewer

func WithApplierBumpReviewer(reviewer fixer.BumpReviewer) ApplierOption

WithApplierBumpReviewer wires an LLM bump reviewer into the applier. It reads the difference between the two versions' upstream build declarations and may ask for MORE validation than policy chose; validate.Escalate applies the proposal and can only ever raise the depth.

A nil reviewer is ignored, exactly as WithApplierFixer ignores a nil fixer and for the same reason: a provider that could not be constructed leaves the applier as it was rather than half-configured, so "no LLM was asked for" and "the LLM could not be built" produce the same, predictable run.

func WithApplierClean

func WithApplierClean(clean bool) ApplierOption

WithApplierClean enables removal of the previous version's ebuild after a successful apply, leaving only the newly created version (and pruning the Manifest's now-orphaned distfile entries). Mirrors the --clean / -c CLI flag.

func WithApplierDepth

func WithApplierDepth(depth validate.Depth) ApplierOption

WithApplierDepth is `--depth`: the one input allowed to REPLACE the class, the package tier and configuration alike, in either direction.

It is the only lowering that is not reported as a skip, because the operator typing it is looking at one package and knows something the policy does not. A caller that omits this option leaves the pointer nil, which is how "the operator asked for nothing" stays distinguishable from "the operator asked for none" — the one confusion that switches validation off in silence.

func WithApplierDistdir

func WithApplierDistdir(explicit, configured string) ApplierOption

WithApplierDistdir supplies the two configurable rungs of the distdir the Manifest step gives pkgdev: explicit is the --distdir flag, configured is autoupdate.distdir from the config file. Both empty — what a caller that omits this option gets — is the production default: the DISTDIR the host's own package manager reports.

func WithApplierDistfilesCache

func WithApplierDistfilesCache(dir string) ApplierOption

WithApplierDistfilesCache supplies the read-only distfiles cache the Manifest step reuses already downloaded files from (--distfiles-cache / autoupdate.distfiles_cache). Empty disables the lookup, and empty is what a caller that omits this option gets.

func WithApplierFixer

func WithApplierFixer(fixer fixer.ManifestFixer) ApplierOption

WithApplierFixer wires an LLM manifest fixer into the applier. When the manifest step fails, the applier asks the fixer to repair the ebuild and then re-runs the manifest to confirm. A nil fixer is ignored, preserving the fail-fast behaviour.

func WithApplierGentooPath

func WithApplierGentooPath(dir string) ApplierOption

WithApplierGentooPath sets the ::gentoo tree consulted before an ebuild is renamed forward. An empty path disables the comparison, which is what tests that do not care about it get by default: a missing tree must not fail a bump.

func WithApplierHTTPClient added in v0.36.0

func WithApplierHTTPClient(c *fetch.RetryableHTTPClient) ApplierOption

WithApplierHTTPClient supplies the client the vendor step probes and downloads distfiles with. Without one the vendor step does nothing.

func WithApplierIsolationProbe

func WithApplierIsolationProbe(fn func() (bool, string)) ApplierOption

WithApplierIsolationProbe replaces the network-namespace probe the compile gate measures with. It defaults to validate.ProbeIsolation; tests supply their own so both answers are reachable without privilege and without root.

func WithApplierLLMConcurrency added in v0.36.0

func WithApplierLLMConcurrency(n int) ApplierOption

WithApplierLLMConcurrency sets how many LLM agents may run at once (default DefaultApplyLLMConcurrency; values below 1 mean 1).

func WithApplierLogger

func WithApplierLogger(l *slog.Logger) ApplierOption

WithApplierLogger sets the logger the applier, and every step it runs, reports its diagnostics to. Nil keeps the default, which discards them. Fixers and the bump reviewer are injected already built, and carry the logger their own options gave them.

func WithApplierOutcomeHint added in v0.36.0

func WithApplierOutcomeHint(hint func(*ApplyResult) string) ApplierOption

WithApplierOutcomeHint makes Apply ask hint for a next step to show beside the package's outcome in the progress reporter, e.g. the command that repairs a failure. The applier only relays the text; an empty answer shows nothing.

func WithApplierPackagesConfig

func WithApplierPackagesConfig(cfg *registry.PackagesConfig) ApplierOption

WithApplierPackagesConfig supplies the per-package autoupdate config so the applier can honour a package's [meta] authenticated-fetch instructions before running the manifest step. A nil config (or one without a matching package) leaves the normal pkgdev-from-SRC_URI behaviour unchanged.

func WithApplierPendingDeleteFunc

func WithApplierPendingDeleteFunc(fn func(pkg string) error) ApplierOption

WithApplierPendingDeleteFunc overrides the function Apply invokes to remove a package from pending.json after a successful apply. The default is a.pending.Delete. This option exists for tests that need to simulate a Delete failure; a nil fn is ignored.

func WithApplierPendingList

func WithApplierPendingList(pending *PendingList) ApplierOption

WithApplierPendingList sets a custom pending list for the applier

func WithApplierQuietGateOutput added in v0.36.0

func WithApplierQuietGateOutput() ApplierOption

WithApplierQuietGateOutput keeps build-gate children's output out of the reporter's task tail. The output is still captured whole and returned.

func WithApplierReporter

func WithApplierReporter(r tui.Reporter) ApplierOption

WithApplierReporter wires a progress reporter into the applier so Apply emits its lifecycle (TaskStart → TaskStage → TaskDone) to the TUI/plain sink. A nil reporter is normalized to tui.Noop(), preserving the silent, fully-buffered behaviour predating the TUI.

func WithApplierRequireIsolation

func WithApplierRequireIsolation(require bool) ApplierOption

WithApplierRequireIsolation makes the compile gate REFUSE to run unisolated.

The setting lives in config.yaml as autoupdate.validate.require_isolation, not in the registry: a registry key is expensive to move once written, and in config.yaml an unknown key is a warning rather than a silently disabled record. It defaults to false, the behaviour before the key existed. This option remains the only way the value reaches the Applier, from the flag or from that key alike.

func WithApplierRequireProof

func WithApplierRequireProof(require bool) ApplierOption

WithApplierRequireProof refuses to promote a bump whose build gates were SKIPPED, for a host where an unproved publish is not acceptable — a builder box, or a maintainer who would rather the sweep stopped than shipped something unbuilt.

It is an opt-in counterweight, not a contradiction: the default publishes such a bump with the depth it did not reach NAMED, because "this host lacks a build dependency" says nothing about the bump, and turning every one of those into a refusal would make the feature inert — the same reason isolation is not required by default.

func WithApplierRunAttached

func WithApplierRunAttached(fn func(cmd *exec.Cmd) ([]byte, error)) ApplierOption

WithApplierRunAttached overrides how the compile test executes. The default (cmd.CombinedOutput) buffers the child's output, which is byte-identical to the pre-TUI behaviour but swallows the sudo/doas password prompt. The apply driver supplies a variant that hands the child the real terminal so the prompt is visible while teeing its raw output to the TTY and a capture buffer (the captured bytes still feed saveCompileLog on failure). A nil fn is normalized back to the CombinedOutput default.

func WithApplierRunCaptured added in v0.36.0

func WithApplierRunCaptured(fn func(cmd *exec.Cmd) error) ApplierOption

WithApplierRunCaptured overrides how a build-gate child that set its own stdin is run. Such a child never gets the terminal: its stdout and stderr are bound to the package's StreamCapture before fn is called, so fn only runs the command (the default is cmd.Run) and whatever it writes to cmd.Stdout reaches the task tail and the returned output. A nil fn is normalized back to cmd.Run.

func WithApplierSetVersionsFunc

func WithApplierSetVersionsFunc(fn func(overlayPath string, pins map[string]string) error) ApplierOption

WithApplierSetVersionsFunc overrides the function Apply invokes to record the applied version in packages.toml after a successful apply. The default is SetPackageVersions. This option exists for tests that need to simulate a write failure or to observe the pin without a registry on disk; a nil fn is ignored. Production callers never supply it.

func WithApplierStagingRoot

func WithApplierStagingRoot(dir string) ApplierOption

WithApplierStagingRoot points the applier at the directory its staged trees are built under, and by doing so turns the staged pipeline on: the candidate is materialised outside the published overlay, every gate reads it there, and the overlay is written exactly once, by promotion, after the gates have passed.

An empty (or blank) root is ignored, exactly as WithApplierFixer ignores a nil fixer and for the same reason: the caller that does not supply one keeps the behaviour it has always had — the candidate written straight into the overlay and rolled back on failure — rather than getting a half-configured pipeline. The production root is <configDir>/staging; Stage never picks one itself, because the path is where the retention rule is recorded.

func WithApplierValidatePolicy

func WithApplierValidatePolicy(policy validate.DepthPolicy) ApplierOption

WithApplierValidatePolicy supplies the configured depth table and its per-package exceptions, which is how far each class of bump is validated.

The policy is expressed in the validate package's own types rather than in config's strings, so translating `autoupdate.validate` — including rejecting a mistyped depth by name through validate.ParseDepth — belongs to the caller. A zero policy is not "no validation": a class with no configured row falls through to the deepest rung, because failing to configure a depth is not evidence that a bump is small.

func WithApplierVendorConfig added in v0.36.0

func WithApplierVendorConfig(cfg config.VendorConfig) ApplierOption

WithApplierVendorConfig supplies the autoupdate.vendor block: the hosts whose Go vendor tarballs the applier owns, and how a generated one is uploaded.

func WithConfirmFunc

func WithConfirmFunc(fn func(prompt string) bool) ApplierOption

WithConfirmFunc sets a custom confirmation function for the applier

func WithExecCommand

func WithExecCommand(fn func(ctx context.Context, name string, arg ...string) *exec.Cmd) ApplierOption

WithExecCommand sets a custom context-aware exec.Command function for testing. The function mirrors exec.CommandContext so injected commands also observe context cancellation.

It replaces the PATH LOOKUP as well, and the two are deliberately one seam. The build gates ask whether `ebuild` and `emerge` exist before spawning them, so that a host with no Portage reports a named SKIP instead of an opaque failure. Leaving that question on the real PATH while the command itself is substituted would let the HOST decide whether the substituted child ever runs — `ebuild` exists on a Gentoo box and nowhere else — so the same code would take different branches on different machines for reasons that have nothing to do with the bump under test. A caller that has replaced how a child process is CREATED has replaced the process layer entire; this makes that true.

func WithLogsDir

func WithLogsDir(dir string) ApplierOption

WithLogsDir sets a custom logs directory for the applier.

type ApplyResult

type ApplyResult struct {
	// Package is the full package name (category/package)
	Package string
	// OldVersion is the version before the update
	OldVersion string
	// NewVersion is the version after the update
	NewVersion string
	// Success indicates whether the apply operation succeeded
	Success bool
	// Error contains any error that occurred during application
	Error error
	// LogPath is the path to the compile log if compilation failed
	LogPath string
	// IsolationVerified reports whether the compile gate PROVED it could create
	// a network namespace before building. It is false when the compile did not
	// run at all.
	//
	// It exists because a compile-gate pass used to claim more than it had. The
	// gate never checked: Portage reports network-sandbox in FEATURES, creating
	// the namespace needs privilege an ordinary user does not have, and Portage
	// says nothing when the unshare fails. So a green could mean "built with the
	// network cut off" or "built with full network access", and nothing printed
	// told them apart.
	IsolationVerified bool
	// IsolationReason says why isolation could not be verified, and is empty
	// exactly when IsolationVerified is true. On a run skipped by
	// --require-isolation it is the reason the compile never happened.
	IsolationReason string
	// CompileDistdir is the directory THIS RUN resolved for the privileged
	// compile gate's archives — the apply's own fetch, then --distdir, then the
	// configured one, whichever staticGateDistdir chose. It is recorded WHATEVER
	// then happened to it, which is the point: a reader who only saw the
	// enforced directory could not tell a run that resolved none from one whose
	// privilege tool could not carry the one it had.
	//
	// It is empty when the compile did not run at all — including the run
	// --require-isolation refused — and when this run resolved no directory of
	// its own, which is the honest answer rather than an invented default:
	// Portage then answers from its own configuration.
	CompileDistdir string
	// CompileDistdirEnforced reports whether that directory actually reached the
	// ebuild child, and it is a separate fact because the privilege tool decides
	// it. Measured on this host: sudo's env_reset
	// discards an exported DISTDIR, so what carries the value is sudo's own
	// argument form, `sudo DISTDIR=<dir> ebuild …`. `doas` has no such form —
	// handed one it would try to execute a program named "DISTDIR=…" — so on a
	// doas host the directory is resolved and NOT enforced.
	//
	// The pair therefore states three things a single field could not: nothing
	// resolved (empty, false), resolved and enforced (dir, true), resolved but
	// uncarriable (dir, false). The compile gate's reason is what says the third
	// one out loud, rather than letting a PASS imply a hermeticity the run never
	// had.
	CompileDistdirEnforced bool
	// CleanedOldVersion is the highest version whose ebuild --clean removed. It
	// is the single-version view of CleanRemoved, kept because callers that
	// print one "Removed: pkg-X.ebuild" line predate the sweep; empty when clean
	// was off, removed nothing, or was blocked.
	CleanedOldVersion string
	// CleanKept maps each version --clean left in place to the registry entry
	// key that claims it. An empty value means the version was kept by a
	// rule rather than by an entry — the live -9999 rule or the last-non-live
	// floor — since a registry key is never itself empty.
	CleanKept map[string]string
	// CleanRemoved lists the versions whose ebuilds --clean actually deleted,
	// ascending. It is the executed plan, not the intended one: a sweep stopped
	// by a removal failure reports the prefix it managed to delete.
	CleanRemoved []string
	// CleanWarning records a non-fatal failure of the --clean step (the update
	// itself still succeeded). Empty on success.
	CleanWarning string
	// RegistryWarning records a non-fatal failure to write the applied version
	// back into packages.toml. It is deliberately NOT CleanWarning:
	// the two report different steps — the pin is written on every successful
	// apply, the sweep only under --clean — and the CLI prints CleanWarning on a
	// line labelled "Clean:", so reusing it would blame the wrong step for a
	// registry failure. Empty when the pin was recorded.
	RegistryWarning string
	// MetadataCacheWarning records a non-fatal failure to regenerate the
	// package's metadata/md5-cache entries after the bump. Empty on success,
	// and when the overlay keeps no md5-cache.
	MetadataCacheWarning string
	// Obsolete indicates the pending entry no longer corresponds to anything to
	// apply: the package was removed from the overlay, or the overlay is already
	// at/beyond the target version. The entry is pruned from pending.json and
	// this is NOT counted as a failure (Success stays false, Error stays nil).
	Obsolete bool
	// Held indicates the package carries hold = true in packages.toml and was
	// therefore refused. Like Obsolete this is NOT a failure (Success false,
	// Error nil), but unlike Obsolete the pending entry is KEPT: the update is
	// real and still pending, it just may not be applied automatically. A held
	// package reaches this point when its update was recorded before the hold
	// was set (CheckPackage skips held packages, but pending.json outlives the
	// check that wrote it).
	//
	// enabled = false is refused the same way and reported through the same
	// field: both are a maintainer's "do not auto-bump", and HoldReason names
	// which of the two keys said so.
	Held bool
	// Waiting lists the requirements a bump is waiting for, as atoms
	// ("~dev-lang/dart-3.14.0"), or the instruction to re-run --check when the
	// pending entry predates them. Like Held it is NOT a failure (Success false,
	// Error nil) and the pending entry is kept: the bump applies once the
	// required version is in the overlay or ::gentoo.
	Waiting []string
	// HoldReason is the packages.toml key that refused the package
	// ("hold = true" or "enabled = false"). Empty unless Held is true.
	HoldReason string
	// ObsoleteReason explains, in user-facing terms, why the entry was deemed
	// obsolete. Empty unless Obsolete is true.
	ObsoleteReason string
	// Fixed indicates a gate failed and was recovered by an LLM fixer: the ebuild
	// was edited and BENTOO'S OWN re-run of that same gate then succeeded. Two
	// gates can set it — the manifest step (`pkgdev manifest`) and the build
	// gate — and in both cases the flag records the re-run's verdict, never the
	// agent's self-report. A bump whose re-run
	// still failed leaves this false, because a "fixed" flag on something still
	// broken is worse than no flag. Only meaningful on the success path.
	Fixed bool
	// FixSummary is the fixer's one-line description of what it changed in the
	// ebuild. Empty unless Fixed is true.
	FixSummary string
	// QASummary holds advisory `pkgcheck` output captured after an LLM fix, when
	// pkgcheck is available. It never changes Success — the apply already passed
	// the authoritative manifest re-run — but surfaces any QA findings the agent's
	// edit may have introduced so a human can review before committing. Empty when
	// pkgcheck is absent, reported nothing, or no fix was applied.
	QASummary string
	// StagedPath is the staged tree this bump was validated in: the
	// single-package repository validate.Stage built, which by construction lives
	// OUTSIDE the published overlay.
	//
	// It is set as soon as the tree exists and kept through every failure, because
	// it is what makes a failure inspectable: the operator can read the exact
	// ebuild the gates read and re-run a gate by hand, without repeating the work
	// that produced it. Retention is expressed by the path itself — one tree per
	// package and version — so there is no index to consult and nothing to unlock.
	// applySummary appends it to the failure line; the CLI prints it as "Staged:".
	//
	// A COMPLETED promotion clears it again: once the bytes are in the overlay the
	// tree says nothing new, and the field keeps its one meaning — "here is the
	// evidence of what went wrong". Empty, therefore, after a successful apply,
	// an apply with no staging root, and one that failed before staging.
	StagedPath string
	// StagedEbuild is the candidate ebuild inside StagedPath. It is set and
	// cleared together with StagedPath.
	StagedEbuild string
	// FailedStage names the step a failed apply stopped in: manifest, llm-fix,
	// re-check, gate:<name>, or one of the run's own stages (substitute,
	// coverage, promotion, promote, retained). "apply" is the fallback for an
	// exit that entered no stage. Empty unless the apply failed.
	FailedStage string
	// RefusedTools lists the tools an LLM fixer was refused during this apply,
	// each label once, in the order they were first refused.
	RefusedTools []string
	// RepairBundle is the repair bundle written beside the kept staged tree of
	// a failed apply. Empty when no bundle was written.
	RepairBundle string
	// DepthRequested is how far this bump was ASKED to be validated: the depth
	// class, tier, configuration and the operator's flags resolved to, raised by
	// a reviewer escalation where one applied.
	//
	// It is a string and not a validate.Depth for the reason
	// validate.EbuildResult states: Depth is an int whose ORDERING is its
	// contract, so it would reach a report as a number and force every reader to
	// carry the ladder to interpret it. The spelling here is the one `--depth`
	// accepts and Depth.String prints.
	DepthRequested string
	// DepthReached is how far validation ACTUALLY got — the deepest rung whose
	// own gate reported PASS, never deeper than DepthRequested.
	//
	// The pair is the rule "an outcome names its own reach" applied to the
	// ladder itself: a bump promoted with its build gates skipped for want of an
	// installed dependency has DepthRequested "configure" and DepthReached
	// short of it, so nobody can read the green as "it builds".
	DepthReached string
	// DepthReason names the input that decided the depth, and — when the two
	// depths differ — why validation stopped short, naming the atoms or the host
	// condition that stopped it. It is never empty on a staged apply.
	DepthReason string
	// ValidationSource says which of the two validation paths this bump took:
	// "staged" when a retained tree that had already been proved was promoted as
	// it stood, "this-run" when the gates ran here. It states per package which
	// of the two happened, carried on the result so the reports and the summary
	// line can both say it.
	//
	// Empty on an apply that ran with no staging root at all, because on that
	// path no gate runs and neither answer would be true. The two constants are
	// ValidationSourceStaged and ValidationSourceThisRun.
	ValidationSource string
	// Vendor is what the vendor step did before the staged manifest: when it
	// generated a Go vendor tarball, the tarball's path, name, URI and digest;
	// when its probe failed, ProbeFailed with the name and URI. The zero value
	// means the step neither generated nor owned anything.
	Vendor VendorOutcome
}

ApplyResult represents the result of applying an update.

type AuthDistfileRequest

type AuthDistfileRequest struct {
	// OverlayPath is the overlay holding .autoupdate/packages.toml and the
	// package's ebuilds. Both are read; nothing in the overlay is written.
	OverlayPath string
	// Package is a "category/package" atom, or a full registry key carrying its
	// ":slot"/"@label" suffix when the atom alone is ambiguous.
	Package string
	// Version is the version to substitute into the record's fetch_filename.
	// Empty means "the version the overlay currently carries", resolved the same
	// way the checker resolves it — highest ebuild, honouring the record's slot
	// and series filters.
	Version string
	// DestDir is the directory the file is written into. It must already exist
	// and be writable; this package does not create it, because the caller that
	// knows whether the answer is a private distdir or the host's own DISTDIR is
	// also the one that must decide whether creating it is appropriate.
	DestDir string
}

AuthDistfileRequest names one authenticated download to perform.

type AuthDistfileResult

type AuthDistfileResult struct {
	// Package is the registry key that was used, which is the requested atom
	// plus whatever suffix the matching record carried. Reported so a caller
	// that passed an atom can name the record it actually acted on.
	Package string
	// Version is the version used, resolved or as requested.
	Version string
	// Path is the file written, inside DestDir.
	Path string
	// SerialEnv names the env var the serial came from, or "" when the record
	// configures no serial. It is the NAME only — the value is never returned,
	// logged or rendered anywhere.
	SerialEnv string
}

AuthDistfileResult describes a completed download.

func FetchAuthDistfile

func FetchAuthDistfile(ctx context.Context, log *slog.Logger, req AuthDistfileRequest) (AuthDistfileResult, error)

FetchAuthDistfile performs the authenticated download a package's [meta] block describes, and reports the file it wrote.

It is the exported form of what the sweep does before `pkgdev manifest`, and runs the same parser, the same guards and the same writer — deliberately, so that a user's fetch and a maintainer's sweep cannot disagree about what the vendor endpoint expects or about what counts as a valid response.

The error is the sentinel-wrapped kind: ErrPackageNotInRegistry, ErrAmbiguousPackageKey and ErrNoAuthFetch describe the request, while ErrAuthFetchSecretMissing and ErrAuthFetchFailed describe the download.

type BatchResult

type BatchResult[T any] struct {
	// Items holds the successfully produced results of the batch operation.
	Items []T
	// Failures maps a package name (category/package) to the error that
	// occurred while processing it.
	Failures map[string]error
}

BatchResult holds the outcome of a batch operation over multiple packages, such as checking or analyzing every configured package. It separates the successfully produced items from the per-package failures so callers can report partial success and derive a process exit code.

Concurrency: BatchResult itself provides no synchronization. When workers populate Failures from multiple goroutines, callers MUST ensure all worker goroutines have joined (for example, after wg.Wait()) before invoking any method on the BatchResult.

func (BatchResult[T]) ExitCode

func (b BatchResult[T]) ExitCode() int

ExitCode returns the process exit code that represents the outcome of the batch operation:

  • 0 when there are no failures.
  • 2 when there are failures and no items were produced (total failure).
  • 1 otherwise (partial failure: some items produced, some failed).

func (BatchResult[T]) FormatFailures

func (b BatchResult[T]) FormatFailures(w io.Writer)

FormatFailures writes one line per recorded failure to w, in the form "ERROR <pkg>: <err>". Failures are emitted sorted lexically by package name so the output is deterministic regardless of map iteration order. Multi-line error strings are flattened: every newline within an error is rewritten to a newline followed by a two-space continuation indent, keeping each failure a single parseable logical record. Each record is terminated with a newline.

Concurrency: FormatFailures is NOT safe to call while other goroutines may still be writing to Failures. Callers MUST invoke it only after every worker goroutine has joined (for example, after wg.Wait()).

func (BatchResult[T]) HasFailures

func (b BatchResult[T]) HasFailures() bool

HasFailures reports whether the batch operation recorded any per-package failures.

type CheckResult

type CheckResult struct {
	// Package is the full package name (category/package)
	Package string
	// CurrentVersion is the version currently in the overlay
	CurrentVersion string
	// UpstreamVersion is the version found upstream
	UpstreamVersion string
	// HasUpdate is true if upstream version is newer than current
	HasUpdate bool
	// NotComparable is true when the upstream value could not be ordered against
	// the current version (e.g. a tag like "INKSCAPE_1_4_4" or an unparseable
	// string). When set, HasUpdate is false and the package was NOT added to the
	// pending list: the result is surfaced as a warning so a silent false
	// "up to date" never masks a real update behind a bad parser config.
	NotComparable bool
	// Error contains any error that occurred during checking
	Error error
	// FromCache is true if the upstream version was retrieved from cache
	FromCache bool
	// Type classifies the package as "bin" or "source", resolved from the
	// config's type field or auto-detected from the ebuild. Empty only when the
	// current ebuild could not be read.
	Type string
	// Orphaned is true when the package no longer has any ebuild in the overlay
	// (getCurrentVersion returned ErrNoEbuildFound). The checker auto-disables
	// the entry (enabled = false) in packages.toml and surfaces the package as
	// an informational result rather than a recurring hard failure. When set,
	// all other fields except Package are zero-valued.
	Orphaned bool
	// Skipped names the packages.toml key ("hold = true" or "enabled = false")
	// that kept CheckPackage from checking the package at all. When set, nothing
	// was fetched, nothing was written to the cache or pending list, and every
	// other field except Package is zero-valued.
	Skipped string
	// Requirements is the state of each `requires` entry captured for this
	// update, in atom order; nil for a record without requires or a bump that
	// was held.
	Requirements []RequirementState
}

CheckResult represents the result of checking a single package for updates.

type Checker

type Checker struct {
	// contains filtered or unexported fields
}

Checker handles version checking operations for packages. It coordinates between configuration, cache, pending list, and upstream sources.

func NewChecker

func NewChecker(overlayPath string, opts ...CheckerOption) (*Checker, error)

NewChecker creates a new checker instance for the given overlay. It loads the packages configuration and initializes cache and pending list.

func (*Checker) Cache

func (c *Checker) Cache() *fetch.Cache

Cache returns the cache instance.

func (*Checker) CheckAll

func (c *Checker) CheckAll(ctx context.Context, force bool) BatchResult[CheckResult]

CheckAll checks all packages in the configuration for updates. If force is true, the cache is bypassed for all packages.

It returns a BatchResult: checked packages land in Items, a per-package failure in Failures keyed by package name.

Packages run concurrently, bounded by WithConcurrency. The semaphore is taken with a context-cancellable select: once ctx is cancelled the remaining packages are not dispatched but recorded with the context error, so a SIGINT stops the batch promptly. Every worker recovers a CheckPackage panic as a failure, and all writes to the shared maps are mutex-guarded. Items are sorted by package name, and the result is returned only after every worker has joined (wg.Wait), so callers may call ExitCode and FormatFailures directly.

func (*Checker) CheckPackage

func (c *Checker) CheckPackage(ctx context.Context, pkg string, force bool) (*CheckResult, error)

CheckPackage checks a single package for updates. If force is true, the cache is bypassed and upstream is queried directly. ctx bounds every upstream fetch, rate-limit wait and LLM call this check makes; the Checker holds no context of its own, so one Checker can serve calls with different lifetimes.

func (*Checker) Config

func (c *Checker) Config() *registry.PackagesConfig

Config returns the packages configuration.

func (*Checker) DisableOrphans

func (c *Checker) DisableOrphans(pkgs []string) error

DisableOrphans marks each package as disabled (enabled = false) and, where the record states no origin yet, records the checker as the origin of that disable (disabled_by = "auto") — an entry that already names a human origin keeps it, or this function would erase the intent it exists to respect — both in the overlay's packages.toml and in the in-memory config, so a package whose ebuild was removed from the overlay stops being processed on subsequent runs — and so the reconciliation in CheckAll can tell this disable from one a human wrote and is free to clear it when the ebuild comes back. The file edit is a single atomic, comment-preserving write for the whole batch. A nil or empty slice is a no-op. The in-memory config is only updated after the file write succeeds, so a failed write leaves both views consistent.

Both keys are mirrored in memory, not just enabled: within a single run the reconciliation READS the in-memory origin, so writing the pair to the file and only half of it to memory would make the checker's own disable look like a legacy human one on the very next scan.

func (*Checker) FindRevivableOrphans

func (c *Checker) FindRevivableOrphans(ctx context.Context, prov provider.Provider) ([]ReviveCandidate, error)

FindRevivableOrphans scans the disabled entries in the config and reports those an autoupdate could revive: the entry's upstream version is strictly newer than the highest version ::gentoo still ships. CheckAll skips disabled entries forever, so without this report a package removed from the overlay would never surface an upstream bump ::gentoo has not caught up to.

A candidate must be BOTH disabled AND absent from the overlay (a true orphan): a disabled entry whose ebuild is still present is not revivable, as a ::gentoo base would seed an older version over the newer one shipped.

An entry whose ":slot" or `series` filter matches NOTHING is skipped just as silently: its directory holds another line, so it is not an orphan, and a disabled line-filtered entry matching nothing is its expected state. A real config mistake is caught on the enabled path (CheckAll, Reconcile's NoEbuild).

Every network call is best-effort: a failed upstream fetch or a package ::gentoo does not carry is skipped; other provider errors become soft notes in the returned error without dropping the candidates gathered. The result is sorted by package name.

func (*Checker) OverlayPath

func (c *Checker) OverlayPath() string

OverlayPath returns the overlay path.

func (*Checker) Pending

func (c *Checker) Pending() *PendingList

Pending returns the pending list instance.

func (*Checker) ReviveDisabled

func (c *Checker) ReviveDisabled(pkgs []string) error

ReviveDisabled re-enables each named package in the overlay's packages.toml and in the in-memory config. It is the inverse of DisableOrphans: once an auto-disabled package's ebuild is back in the overlay — the source of truth for whether a package exists — it is reconciled to enabled. The file edit is a comment-preserving DELETION of both keys of the disable, `enabled = false` and its `disabled_by` origin: enabled is spelled by the key's absence, so writing `enabled = true` would leave a redundant-enabled finding for --lint --fix, and a leftover origin would claim a state the record is no longer in. A nil or empty slice is a no-op. The in-memory config changes only after the file write succeeds, so a failed write leaves both views consistent — including the origin, which the reconciliation reads later in the same run.

Callers must exclude held packages (hold = true): a hold is an explicit maintainer decision that the overlay reconciliation must never flip. Callers must likewise pass only packages reconcilesAutomatically accepts; this function performs the revive it is asked for and does not second-guess it.

type CheckerOption

type CheckerOption func(*Checker) error

CheckerOption is a functional option for configuring Checker

func WithCache

func WithCache(cache *fetch.Cache) CheckerOption

WithCache sets a custom cache for the checker

func WithCacheTTL

func WithCacheTTL(d time.Duration) CheckerOption

WithCacheTTL sets the TTL applied to the default Cache constructed by NewChecker when no Cache is injected via WithCache. It enables `autoupdate.cache_ttl` from ~/.config/bentoo/config.yaml to reach Cache.TTL . A non-positive duration is rejected at construction time, mirroring WithOpTimeout's validation; the CLI guards the value upstream via AutoupdateConfig.GetCacheTTL, so this is defence-in-depth for direct callers.

func WithConcurrency

func WithConcurrency(n int) CheckerOption

WithConcurrency sets the maximum number of packages CheckAll processes in parallel. n must be in the inclusive range [1, maxConcurrency]; a value outside that range is rejected. When this option is not supplied the Checker uses DefaultConcurrency.

func WithConfigDir

func WithConfigDir(dir string) CheckerOption

WithConfigDir sets the configuration directory for cache and pending files

func WithFetchCache

func WithFetchCache(enabled bool) CheckerOption

WithFetchCache turns per-run deduplication of upstream response bodies on or off. It is ON by default — NewChecker builds a cache in its struct literal — so the only reason to pass this option at all is to turn it OFF.

DISABLED IS THE ABSENCE OF A CACHE, not a flag consulted on the fetch path: false sets the field to nil, and fetchContent calls fetchContentUncached directly. That makes the off switch a trustworthy bisection tool — the disabled path is not a second implementation to audit, it is the uncached code byte for byte, with no join, no sharing and no retention.

true INSTALLS a cache only when none is present, so the option is idempotent: applied after the default construction it keeps the cache already there with its accumulated bodies and counters, instead of discarding them and paying for every earlier read a second time.

It cannot fail: a bool has no invalid value. The error in the return type is the CheckerOption signature, not a possibility.

func WithGentooPath

func WithGentooPath(path string) CheckerOption

WithGentooPath sets the ::gentoo tree consulted, after the overlay, when the check report decides whether a required version is present.

func WithGitHubTokenErrorSink added in v0.35.0

func WithGitHubTokenErrorSink(sink func(error)) CheckerOption

WithGitHubTokenErrorSink hands the error of the checker's GitHub token resolution to sink instead of logging it as a warning, so a caller that builds several checkers, or reads the same secrets file elsewhere, can report the failure once. The checker still continues with unauthenticated access. A nil sink keeps the default: the checker logs its own warning.

func WithHTTPClient

func WithHTTPClient(client *fetch.RetryableHTTPClient) CheckerOption

WithHTTPClient sets a custom HTTP client for the checker

func WithHTTPRequestTimeout

func WithHTTPRequestTimeout(d time.Duration) CheckerOption

WithHTTPRequestTimeout sets the per-request HTTP timeout — the cap on a single outbound attempt. NewChecker applies it to the HTTP client and, unless WithOpTimeout overrode the budget explicitly, derives the per-operation budget from it so every retry attempt fits within the deadline (see deriveOpTimeout). A non-positive duration is a no-op (the client keeps its default), so callers that wire this option unconditionally can pass an unresolved zero safely.

func WithLLMClient

func WithLLMClient(llm llm.LLMProvider) CheckerOption

WithLLMClient sets the LLM provider used by --check's version-extraction fallback. It accepts any LLMProvider (AD2), so a non-claude provider — which the pre-refactor *LLMClient signature could not express — is now valid; the legacy *LLMClient still satisfies the interface and remains accepted.

A nil provider is ignored (the field is left untouched, i.e. nil), mirroring WithRateLimiter's nil rejection. This is defence-in-depth: the CLI must wire this option only with a successfully constructed, non-nil provider, because a typed-nil interface (a nil concrete pointer boxed by a failed constructor) would pass `!= nil` and make fetchUpstreamVersion call ExtractVersion on a nil receiver. Refusing nil here keeps llmClient an untyped nil when no usable provider exists.

func WithLLMProviderConfigured

func WithLLMProviderConfigured(configured bool) CheckerOption

WithLLMProviderConfigured records whether the CLI attempted to configure an LLM provider for this run (true when autoupdate.llm.provider was non-empty), independent of whether the provider was successfully built and wired via WithLLMClient. It exists to gate the "unused llm_prompt" Warn so that warning fires only when NO provider was configured; when a provider was configured but failed to construct, runCheck logs its own failure Warn and this flag suppresses the duplicate construction Warn. Defaults false, so callers that omit it preserve the pre-refactor warn behaviour.

func WithLogger

func WithLogger(l *slog.Logger) CheckerOption

WithLogger sets the logger the checker reports its diagnostics to, and hands it to the HTTP client the checker uses. Nil keeps the default, which discards them (and leaves an injected client's own logger alone).

func WithOpTimeout

func WithOpTimeout(d time.Duration) CheckerOption

WithOpTimeout sets the per-operation timeout used to derive a child context for each outbound fetch. A non-positive duration is rejected. Setting it marks the budget as explicit, so NewChecker will not overwrite it with the value derived from WithHTTPRequestTimeout.

func WithPackagesConfig

func WithPackagesConfig(config *registry.PackagesConfig) CheckerOption

WithPackagesConfig sets a custom packages configuration

func WithPendingList

func WithPendingList(pending *PendingList) CheckerOption

WithPendingList sets a custom pending list for the checker

func WithPerHostConcurrency

func WithPerHostConcurrency(n int) CheckerOption

WithPerHostConcurrency caps the requests in flight to one host at n; n < 1 removes the cap.

func WithProgressCallback

func WithProgressCallback(cb ProgressCallback) CheckerOption

WithProgressCallback sets a callback invoked once per package as CheckAll completes that package's work. A nil callback disables progress reporting. See ProgressCallback for the concurrency contract.

func WithRateLimiter

func WithRateLimiter(limiter httpRateLimiter) CheckerOption

WithRateLimiter sets a custom HTTP rate limiter for the checker. The limiter is consulted (per host) before every outbound fetch. A nil limiter is rejected; when this option is not supplied NewChecker installs a default limiter, so the Checker's rateLimiter is never nil after construction.

func WithTypeFilter

func WithTypeFilter(t string) CheckerOption

WithTypeFilter restricts CheckAll to packages of the given type, "bin" or "source". An empty string (the default) checks every package. Type is resolved from each package's configured type field, falling back to ebuild auto-detection. An unrecognized value is rejected.

type ChildRunError added in v0.36.0

type ChildRunError struct {
	// Label names the run, e.g. "first manifest run".
	Label string
	// Output is the last tui.MaxCapturedBytes the run printed, stdout and
	// stderr interleaved, with every resolved secret masked. When the run
	// printed more, a first line states how many earlier bytes were dropped.
	Output string
	// LogPath is the file holding everything the run printed, from its first
	// byte, with every resolved secret masked; empty when it could not be
	// written or no logs directory was configured.
	LogPath string
	// Err is how the run ended (an *exec.ExitError, a timeout); Unwrap returns
	// it, so errors.Is/errors.As still see the process failure.
	Err error
}

ChildRunError is one failed child process run: what the run was, the tail of what it printed, and where the whole of it was kept.

Error() is a single line — the label, the run error and the first line of Output that reads as an error — so an error holding several runs stays short wherever it is printed whole. The output itself travels in Output, its last tui.MaxCapturedBytes, for a renderer that bounds it, and in the file at LogPath, which keeps all of it.

func ChildRuns added in v0.36.0

func ChildRuns(err error) []*ChildRunError

ChildRuns returns every distinct *ChildRunError in err's chain, following both single and multi-error Unwrap, in the order they are first reached. A run reachable along two branches is returned once.

func (*ChildRunError) Error added in v0.36.0

func (e *ChildRunError) Error() string

func (*ChildRunError) Unwrap added in v0.36.0

func (e *ChildRunError) Unwrap() error

type DataSource

type DataSource struct {
	// URL is the endpoint to query for version information
	URL string
	// Type identifies the source type: "github", "pypi", "npm", "crates", "homepage", "provided"
	Type string
	// Priority determines the order of sources (lower is higher priority)
	Priority int
	// ContentType is the expected content type (e.g., "application/json", "text/html")
	ContentType string
}

DataSource represents a candidate data source for version checking. It contains information about where to fetch version information and how to prioritize different sources.

func DiscoverDataSources

func DiscoverDataSources(meta *ebuilds.EbuildMetadata, providedURL string) []DataSource

DiscoverDataSources finds candidate URLs for version checking. It analyzes ebuild metadata and returns a prioritized list of data sources. If providedURL is non-empty, it is included as the highest priority source.

type Divergence

type Divergence struct {
	// Key identifies what the divergence is about: the registry key
	// ("net-libs/webkit-gtk:4.1") for StalePin and NoEbuild, the bare atom of
	// the directory for UnclaimedEbuild — see that constant for why. Either way
	// it is identity only: never build a path from it, always split it first.
	Key string
	// Kind is which of the three classes this is.
	Kind DivergenceKind
	// Pin is the version the registry declares (PackageConfig.Version), empty
	// when the entry has no pin — which is every entry today.
	Pin string
	// Disk is the version the overlay actually holds: the ebuild the entry
	// resolves to (StalePin), or the stray ebuild itself (UnclaimedEbuild).
	// Empty for NoEbuild, where the point is that there is none.
	Disk string
}

Divergence is one disagreement between what the registry claims and what the overlay holds.

The invariants a consumer may rely on, per kind:

StalePin        Key = registry key · Disk != "" · Disk != Pin  → writable
UnclaimedEbuild Key = "category/package" atom · Pin = ""       → report only
NoEbuild        Key = registry key · Disk = ""                 → report only

func Reconcile

func Reconcile(log *slog.Logger, overlayPath string, cfgs map[string]registry.PackageConfig) []Divergence

Reconcile compares every enabled entry's pin against the ebuild it resolves to on disk, for the whole registry, and returns the divergences in three classes: StalePin, UnclaimedEbuild and NoEbuild.

It only ever reads. The registry is a published artifact — the overlay auto-commits and pushes, so a wrong pin is a released one — so the caller shows the whole set, takes ONE confirmation for all of it, leaves packages.toml byte-identical on "no" and refuses to write from a non-TTY without --yes. Nothing here should ever start writing packages.toml.

Only enabled entries can be the SUBJECT of a divergence, but the unclaimed scan counts every entry of a directory: a switched-off entry still holds its file. Resolution goes through selectCurrentEbuild; its not-found sentinels land in NoEbuild, and any other error skips that entry with a warning — a fabricated divergence would become a fabricated, published pin. The result is sorted, so two runs differ only when the overlay did. It costs about 2-3 readdir per enabled entry; CheckResult could not replace the scan, as it carries no directory listing. Skips are warned about to log; nil discards them.

type DivergenceKind

type DivergenceKind int

DivergenceKind classifies one disagreement between the registry and the overlay. The three kinds are NOT interchangeable: only StalePin carries a version the reconciliation may write, so a consumer that builds a write batch MUST switch on the kind rather than map every divergence to Key -> Disk.

const (
	// StalePin — the entry resolves to an ebuild whose version is not the one
	// it declares. Disk is the version on disk (never empty) and Pin is what the
	// registry says today.
	//
	// Pin is empty for the whole first reconciliation: all 409 records are
	// pinless right now, so an enabled entry that resolves to an ebuild diverges
	// from its (absent) pin. That is deliberate and load-bearing — it is exactly
	// the ~317-entry first bulk fill, and the only class the write batch is
	// built from. A caller wording the prompt can still tell the two apart
	// (Pin == "" reads "pin 317 entries for the first time", Pin != "" reads
	// "correct N stale pins"); the reconciliation itself does not, because the
	// repair is identical: write Disk.
	StalePin DivergenceKind = iota
	// UnclaimedEbuild — a non-live ebuild that no entry of its directory pins or
	// resolves to.
	//
	// It is a property of a DIRECTORY, not of an entry: several entries share
	// one directory (":slot" and "@label" siblings), and reporting the same
	// stray file once per sibling would inflate the prompt's count of what is
	// about to happen. So it is emitted ONCE per file, and Key holds the bare
	// "category/package" atom rather than a registry key. Pin is always empty —
	// nothing pins it, that IS the finding — and nothing about it is writable:
	// the repair is a sweep or a new registry entry, both decided by a human.
	UnclaimedEbuild
	// NoEbuild — the entry is enabled but its directory holds no ebuild it can
	// select: the package was removed, or its ":slot"/`series` filter matches
	// nothing there.
	//
	// Disk is always empty, so there is nothing to write (the registry never
	// holds a version that is not on disk) and reporting it is the whole of the
	// action. The existing orphan reconciliation, not this one, is what acts on
	// a removed package.
	NoEbuild
)

func (DivergenceKind) String

func (k DivergenceKind) String() string

String renders a kind as the stable identifier a report and a filter can both use, in the kebab-case the lint rules already use.

type FallbackSuggestion

type FallbackSuggestion struct {
	// ParserType is the type of fallback parser
	ParserType string
	// Reliability is the reliability score (lower is better)
	Reliability ParserReliability
	// Reason explains why this fallback is suggested
	Reason string
}

FallbackSuggestion represents a suggested fallback parser configuration.

func GetBestFallback

func GetBestFallback(primaryParser string) *FallbackSuggestion

GetBestFallback returns the single best fallback for a given primary parser. It returns the most reliable fallback that is different from the primary parser.

func OrderFallbacksByReliability

func OrderFallbacksByReliability(fallbacks []FallbackSuggestion) []FallbackSuggestion

OrderFallbacksByReliability sorts fallback suggestions by reliability. This ensures fallbacks are tried in order of reliability (JSON > HTML > regex > LLM).

func SuggestFallbacks

func SuggestFallbacks(primaryParser string) []FallbackSuggestion

SuggestFallbacks suggests appropriate fallback parsers based on the primary parser type. It returns fallbacks ordered by reliability (JSON > HTML > regex > LLM). The primary parser type is excluded from suggestions.

type ParserReliability

type ParserReliability int

ParserReliability defines the reliability order of parsers. Lower values indicate higher reliability. Order: JSON (1) > HTML (2) > regex (3) > LLM (4)

const (
	// ReliabilityJSON is the highest reliability (structured data)
	ReliabilityJSON ParserReliability = 1
	// ReliabilityHTML is second highest (semi-structured)
	ReliabilityHTML ParserReliability = 2
	// ReliabilityRegex is third (pattern matching)
	ReliabilityRegex ParserReliability = 3
	// ReliabilityLLM is lowest (AI-based extraction)
	ReliabilityLLM ParserReliability = 4
)

func GetParserReliability

func GetParserReliability(parserType string) ParserReliability

GetParserReliability returns the reliability score for a parser type. Lower scores indicate higher reliability.

type PendingList

type PendingList struct {
	// Updates holds all pending updates, keyed by package name
	Updates map[string]PendingUpdate `json:"updates"`
	// contains filtered or unexported fields
}

PendingList manages the list of pending updates. It persists updates to disk and supports concurrent access.

func NewPendingList

func NewPendingList(configDir string, opts ...PendingListOption) (*PendingList, error)

NewPendingList creates or loads a pending list from disk. If the pending file exists, it loads existing entries. If the pending file doesn't exist or is corrupted, it creates a new empty list. The configDir should be the bentoo config directory (e.g., ~/.config/bentoo/autoupdate).

func (*PendingList) Add

func (p *PendingList) Add(update PendingUpdate) error

Add adds or updates a pending update. If the package already exists, it updates the entry. It automatically saves the pending list to disk after adding.

func (*PendingList) Clear

func (p *PendingList) Clear() error

Clear removes all entries from the pending list. It automatically saves the pending list to disk after clearing.

func (*PendingList) Delete

func (p *PendingList) Delete(pkg string) error

Delete removes a package from the pending list. It automatically saves the pending list to disk after deletion.

func (*PendingList) Get

func (p *PendingList) Get(pkg string) (*PendingUpdate, bool)

Get retrieves a pending update by package name. Returns the update and true if found, zero value and false otherwise.

func (*PendingList) Has

func (p *PendingList) Has(pkg string) bool

Has checks if a package exists in the pending list.

func (*PendingList) Len

func (p *PendingList) Len() int

Len returns the number of entries in the pending list.

func (*PendingList) List

func (p *PendingList) List() []PendingUpdate

List returns all pending updates as a slice. The returned slice contains copies of the updates.

func (*PendingList) ListByStatus

func (p *PendingList) ListByStatus(status UpdateStatus) []PendingUpdate

ListByStatus returns all pending updates with the specified status.

func (*PendingList) Save

func (p *PendingList) Save() error

Save persists the pending list to disk. This is thread-safe and can be called concurrently.

func (*PendingList) SetStatus

func (p *PendingList) SetStatus(pkg string, status UpdateStatus, errMsg string) error

SetStatus updates the status of a pending update. The errMsg parameter is used to set the Error field when status is StatusFailed. Returns ErrPackageNotInPending if the package is not found.

type PendingListOption

type PendingListOption func(*PendingList)

PendingListOption is a functional option for configuring PendingList

func WithPendingNowFunc

func WithPendingNowFunc(fn func() time.Time) PendingListOption

WithPendingNowFunc sets a custom time function for testing

type PendingUpdate

type PendingUpdate struct {
	// Package is the full package name (category/package)
	Package string `json:"package"`
	// CurrentVersion is the version currently in the overlay
	CurrentVersion string `json:"current_version"`
	// NewVersion is the upstream version detected
	NewVersion string `json:"new_version"`
	// CommitHash is the upstream commit SHA for snapshot packages tracked via
	// track="commit". When non-empty, Apply substitutes the old commit variable
	// (EGIT_COMMIT / GIT_COMMIT / COMMIT) in the copied ebuild with this hash.
	CommitHash string `json:"commit_hash,omitempty"`
	// AuxValue is the free-text value captured via the package's aux_pattern
	// (e.g. "esr-bb24"). When non-empty, Apply substitutes the aux_var assignment
	// in the copied ebuild with this value.
	AuxValue string `json:"aux_value,omitempty"`
	// Requires maps each package the record requires ("dev-lang/dart") to the
	// version captured for it from the same upstream release ("3.14.0"). Empty
	// for a record without `requires`, and absent from entries written before
	// the field existed — the applier treats that absence on a record that does
	// declare requires as "not captured", never as "no requirement".
	Requires map[string]string `json:"requires,omitempty"`
	// Status is the current status of this update
	Status UpdateStatus `json:"status"`
	// DetectedAt is when this update was first detected
	DetectedAt time.Time `json:"detected_at"`
	// Error contains error message if status is failed
	Error string `json:"error,omitempty"`
}

PendingUpdate represents a detected update awaiting application.

type ProgressCallback

type ProgressCallback func(done, total uint64)

ProgressCallback reports batch progress as a check proceeds. It is invoked once per package as that package's work completes, with done being the cumulative count of packages finished so far and total the number of packages in the batch.

Because CheckAll runs packages concurrently, the callback may fire from multiple goroutines and the per-invocation order is not deterministic. However done is sourced from an atomic counter, so the value observed by any single invocation is monotone non-decreasing: each callback sees a strictly larger done than every callback that ran before it.

type RegistryFixAttempt

type RegistryFixAttempt struct {
	// Package is the package the attempt was for.
	Package string
	// Status is the attempt's outcome.
	Status RegistryFixStatus
	// Stage is the step a Skipped or Reverted attempt stopped at; zero for
	// Passed and StillFailing.
	Stage RegistryFixStage
	// Result is the agent's result (Model, Summary, DeniedTools); nil when the
	// fixer never ran.
	Result *fixer.RegistryFixResult
	// Recheck is the fresh re-check's result; nil unless a re-check ran.
	Recheck *CheckResult
	// RecheckErr is the still-failing error shown to the operator: the
	// re-check's own Error when it is set, the CheckPackage error otherwise.
	RecheckErr error
	// Err is the RAW cause of a Skipped or Reverted attempt, with no extra
	// wrapping, so the caller's printed %v text is unchanged; nil otherwise.
	Err error
	// RestoreErr is set when the automatic restore of a Reverted attempt failed.
	RestoreErr error
	// contains filtered or unexported fields
}

RegistryFixAttempt is the data record of one snapshot-guarded registry fix. It carries no terminal output: the caller decides what to print from Status, Stage and the errors.

func AttemptRegistryFix

func AttemptRegistryFix(ctx context.Context, overlayPath, pkg string, fetchErr error,
	registryFixer fixer.RegistryFixer, newChecker func() (*Checker, error)) *RegistryFixAttempt

AttemptRegistryFix runs one snapshot-guarded fix for pkg: it captures the raw packages.toml bytes and mode, hands the package's current entry to registryFixer, and re-checks the package through a FRESH Checker from newChecker so the edited config is reloaded from disk. Success is decided by that re-check, never by the agent's summary.

It never returns with packages.toml in a half-edited state except when Status is RegistryFixStillFailing, which the caller settles with Revert or by keeping the edit. A fixer or checker-construction error restores the snapshot before returning; a failure of that restore is reported in RestoreErr, not returned early. Nothing here prints.

func (*RegistryFixAttempt) Revert

func (a *RegistryFixAttempt) Revert() error

Revert restores the pre-attempt packages.toml bytes and permission bits atomically, through the shared fileutil.WriteFileAtomic — the helper every other registry writer uses — so the restored registry carries exactly the captured mode whatever the umask and whatever stale temporary file sits beside it. A Skipped attempt (the zero Status) never edited the file and may hold no snapshot, so reverting it is a no-op rather than a write of empty bytes.

type RegistryFixStage

type RegistryFixStage int

RegistryFixStage names the step a Skipped or Reverted attempt stopped at, so the caller prints the same line it prints today for each of the four. It is zero for Passed and StillFailing.

const (
	// RegistryFixStageSnapshot: the read or stat of packages.toml failed → Skipped.
	RegistryFixStageSnapshot RegistryFixStage = iota + 1
	// RegistryFixStageLoad: LoadPackagesConfig failed → Skipped.
	RegistryFixStageLoad
	// RegistryFixStageFixer: FixRegistry returned an error → Reverted.
	RegistryFixStageFixer
	// RegistryFixStageChecker: the re-check checker could not be built → Reverted.
	RegistryFixStageChecker
)

type RegistryFixStatus

type RegistryFixStatus int

RegistryFixStatus is the outcome of one AttemptRegistryFix.

const (
	// RegistryFixSkipped means no edit was made: the snapshot or the load of
	// packages.toml failed, so the fixer never ran and nothing was written.
	RegistryFixSkipped RegistryFixStatus = iota
	// RegistryFixReverted means the fixer or the re-check setup failed and the
	// snapshot has already been restored (or RestoreErr says why it could not be).
	RegistryFixReverted
	// RegistryFixPassed means the fresh re-check extracted a version; the edit
	// is kept.
	RegistryFixPassed
	// RegistryFixStillFailing means the re-check still fails; the edit is on
	// disk and the caller settles it by keeping it or calling Revert.
	RegistryFixStillFailing
)

type RequirementState

type RequirementState struct {
	// Package is the required "category/package".
	Package string
	// Version is the version captured for it.
	Version string
	// State is "present" (the overlay or ::gentoo satisfies the pin), "pending"
	// (a pending entry of that package will) or "missing" (neither).
	State string
}

RequirementState is whether the version a bump requires is available yet.

type ReviveApplier

type ReviveApplier interface {
	SeedFromGentoo(pkg, srcDir, version string) error
	// MarkReenabled tells the applier the entry was re-enabled after it loaded
	// packages.toml, so its stale enabled = false does not refuse the bump.
	MarkReenabled(pkg string)
	Apply(ctx context.Context, pkg string, compile bool) (*ApplyResult, error)
}

ReviveApplier is the part of *Applier the pipeline drives. It is an interface so a unit test can make Apply report an obsolete bump.

type ReviveCandidate

type ReviveCandidate struct {
	// Package is the full package name (category/package).
	Package string
	// GentooVersion is the highest version found in ::gentoo for the package.
	GentooVersion string
	// UpstreamVersion is the version reported by the package's upstream source.
	UpstreamVersion string
}

ReviveCandidate describes a disabled (orphaned) packages.toml entry whose upstream release is strictly newer than the highest version ::gentoo still carries. It is a passive report: FindRevivableOrphans never mutates the config or the overlay, it only flags entries a later revive step could resurrect.

type ReviveOutcome

type ReviveOutcome struct {
	Package string
	Status  ReviveStatus
	Detail  string
}

ReviveOutcome is the result of reviving one package. Detail is the human-facing note: "<gentoo> → <upstream>" when revived, the skip reason, or the failing step's error.

type ReviveStatus

type ReviveStatus string

ReviveStatus is the result class of reviving one package.

const (
	ReviveRevived ReviveStatus = "revived"
	ReviveSkipped ReviveStatus = "skipped"
	ReviveWaiting ReviveStatus = "waiting"
	ReviveFailed  ReviveStatus = "failed"
)

The revive results the summary keys on. ReviveWaiting is a bump whose required versions are not available yet: the entry is re-enabled and the bump stays pending, so it is not a failure.

type Reviver

type Reviver struct {
	// contains filtered or unexported fields
}

Reviver resurrects orphaned packages one at a time: it seeds the highest ::gentoo ebuild into the overlay, re-enables the packages.toml entry, re-checks upstream with the cache bypassed and applies the bump. One Reviver serves every target of a run.

func NewReviver

func NewReviver(overlayPath string, applier ReviveApplier, prov provider.Provider,
	newChecker func() (*Checker, error), opts ...ReviverOption) (*Reviver, error)

NewReviver returns ErrNoLocalPackageDir (wrapped) when prov is not a provider.PackageDirProvider. newChecker must build a Checker that shares the applier's PendingList: the entry CheckPackage writes is read back by Apply in the same process, from the same in-memory map.

func (*Reviver) Revive

func (r *Reviver) Revive(ctx context.Context, pkg string) ReviveOutcome

Revive performs the full revive of one package and returns its outcome. It never returns an error: every failure is an outcome, so the caller can continue with the remaining targets.

Steps, in order: locate the ::gentoo package dir, pick the highest ::gentoo version, seed it into the overlay, re-enable the entry in packages.toml BEFORE checking (so the checker does not skip it), CheckPackage(force=true) on a fresh Checker to populate pending with the upstream version, then Apply.

type ReviverOption

type ReviverOption func(*Reviver)

ReviverOption configures a Reviver.

func WithReviveCompile

func WithReviveCompile(compile bool) ReviverOption

WithReviveCompile passes the compile request to the apply of every revived bump (--compile).

type ScriptParser

type ScriptParser struct {
	URL     string
	Script  string
	Headers map[string]string
	// contains filtered or unexported fields
}

ScriptParser extracts a version by evaluating JS against a live page. Unlike the Parser implementations it navigates itself (it needs the rendered DOM), so it is driven by Checker.parseLive rather than the NewParserFromConfig path and the Parser interface (there is no pre-fetched []byte to hand it).

func (*ScriptParser) ParseLive

func (p *ScriptParser) ParseLive(ctx context.Context) (string, error)

ParseLive renders URL and evaluates Script, returning the trimmed result. The script is responsible for producing a Gentoo-formatted version string (it may do its own transform/selection in JS) — transform/select from the TOML do not apply on this path.

type SpaceFunc

type SpaceFunc func(dir string) (uint64, error)

SpaceFunc reports how many bytes are still available to the INVOKING user on the filesystem backing dir. It is the seam that makes the full-filesystem branch testable without filling a real disk.

Contract:

  • the result is a byte count, not blocks, and it is the space available to an unprivileged user — the reserved blocks root can still use are not ours and must not be counted;
  • a non-nil error means the question could not be answered. It does NOT mean "no space". ClassifyManifestFailure treats an unanswerable check as no evidence and moves on, because a classifier that manufactured an "environment" verdict out of a failed statfs would gate away a repair on the strength of knowing nothing.

type SweepBatch

type SweepBatch struct {
	// Dirs holds one entry per candidate directory, sorted by Atom.
	Dirs []SweepDirPlan
	// PlanErrors holds the directories that could not be planned.
	PlanErrors []SweepPlanError
	// SkippedHeld holds the atoms that DO hold an unclaimed ebuild but are not
	// swept because a held entry covers them, sorted.
	//
	// It exists so the tool cannot contradict itself. The reconciliation ends
	// its unclaimed group by pointing at `--clean`, and without this
	// field a sweep whose only findings were held would answer "every ebuild is
	// claimed by an entry" — which is false twice over: the ebuild is unclaimed,
	// and it is being protected rather than overlooked. Skipping quietly is what
	// makes a safety rule read as a bug.
	SkippedHeld []string
	// TotalRemove is the number of files the batch would delete. It counts
	// Remove only — a blocked directory's WouldRemove is not pending work.
	TotalRemove int
}

SweepBatch is everything an overlay sweep intends to do, ordered so two runs over an unchanged overlay produce the identical plan.

func PlanOverlaySweep

func PlanOverlaySweep(log *slog.Logger, overlayPath string, cfgs map[string]registry.PackageConfig, target string) (SweepBatch, error)

PlanOverlaySweep computes what a sweep would do to every package directory holding an ebuild no entry claims, for the whole overlay or for one target.

The candidates come from Reconcile, which already reports this class of finding (UnclaimedEbuild) with its enabled filter and the bare atom as Key, so what a sweep touches is what `--check` printed — parity by construction, not by two implementations agreeing. The one deliberate break is `hold`, in the reporting direction only: see the filter below.

Why the verdict does NOT come from Reconcile

A divergence says a file is unclaimed; whether removing it is allowed is planSweep's question alone — the live-ebuild rule, the last-non-live floor and both block cases live there, and deleting straight from the divergence list would drop all of them.

Nothing here touches the filesystem beyond reading directories. The registry's reconciliation reports what it skips to log; nil discards it.

type SweepDirPlan

type SweepDirPlan struct {
	// Atom is "category/package" — never a registry key. Paths are built from
	// it, so a ":slot" or "@label" here would be destructive.
	Atom string
	// Remove lists the versions to delete, ascending in Gentoo order. Empty on
	// any blocked plan.
	Remove []string
	// Keep maps a kept version to the entry key claiming it. An empty value
	// means the version is kept by a rule (the live-ebuild rule or the
	// last-non-live floor) rather than by an entry.
	Keep map[string]string
	// Blocked names the entry that lacks a pin, when there is one to name. A
	// directory blocked because NO entry claims it has an empty Blocked — see
	// sweepPlan's type comment for why the two are distinguishable.
	Blocked string
	// WouldRemove lists what a blocked directory left alone, ascending.
	WouldRemove []string
}

SweepDirPlan is one package directory's verdict in an overlay sweep.

Exactly one of Remove / WouldRemove carries versions: a blocked directory removes nothing and reports its candidates in WouldRemove instead, so a report can state what an authorised sweep would have done without ever implying it happened.

func (SweepDirPlan) IsBlocked

func (p SweepDirPlan) IsBlocked() bool

IsBlocked reports whether this directory will remove nothing. It is true for both block cases — a pinless entry (Blocked names it) and a directory no entry claims (Blocked is empty) — which is why callers must not test Blocked != "" on its own.

type SweepDirResult

type SweepDirResult struct {
	// Atom is the directory, as a bare "category/package".
	Atom string
	// Removed lists the versions that really went away — never what was merely
	// intended. A report that claims a file still on disk is worse than no
	// report.
	Removed []string
	// Kept mirrors the plan's Keep so a report can name the entry claiming each
	// surviving version.
	Kept map[string]string
	// Blocked names the entry that blocked this directory, when there was one.
	Blocked string
	// WouldRemove lists what a blocked directory left alone.
	WouldRemove []string
	// Err is this directory's failure. It never aborts the batch.
	Err error
}

SweepDirResult is what actually happened to one package directory.

type SweepOption

type SweepOption func(*sweepOptions)

SweepOption configures ExecuteOverlaySweep.

func WithSweepConcurrency

func WithSweepConcurrency(n int) SweepOption

WithSweepConcurrency bounds how many directories are swept at once.

func WithSweepDistdir

func WithSweepDistdir(explicit, configured string) SweepOption

WithSweepDistdir supplies the two configurable rungs of the distdir precedence: explicit is the --distdir flag, configured is autoupdate.distdir from the config file. Both empty is the production default and resolves to the DISTDIR the host itself names.

func WithSweepDistfilesCache

func WithSweepDistfilesCache(dir string) SweepOption

WithSweepDistfilesCache supplies the read-only cache the manifest step reuses already downloaded distfiles from (--distfiles-cache / autoupdate.distfiles_cache). Empty disables the lookup.

func WithSweepExecCommand

func WithSweepExecCommand(fn func(ctx context.Context, name string, arg ...string) *exec.Cmd) SweepOption

WithSweepExecCommand injects the command seam (tests, and nothing else).

func WithSweepLogger

func WithSweepLogger(l *slog.Logger) SweepOption

WithSweepLogger sets the logger the sweep reports its diagnostics to. Nil keeps the default, which discards them.

func WithSweepPackagesConfig

func WithSweepPackagesConfig(cfgs map[string]registry.PackageConfig) SweepOption

WithSweepPackagesConfig supplies the registry, read only for the [meta] block that drives an authenticated distfile fetch.

func WithSweepReporter

func WithSweepReporter(r tui.Reporter) SweepOption

WithSweepReporter routes the Manifest step's streamed output.

type SweepPlanError

type SweepPlanError struct {
	Atom string
	Err  error
}

SweepPlanError is a directory that could not be planned at all — an unreadable package directory, not a directory with nothing to do. It is reported and dropped from the batch rather than aborting the sweep.

type SweepReport

type SweepReport struct {
	// Results holds one entry per planned directory, sorted by Atom.
	Results []SweepDirResult
	// Removed counts the files that went away across the batch.
	Removed int
	// Swept counts directories from which at least one file was removed.
	Swept int
	// BlockedN counts directories that removed nothing because they were
	// blocked.
	BlockedN int
	// Failed counts directories whose removal or Manifest step errored.
	Failed int
}

SweepReport is the outcome of an executed batch.

func ExecuteOverlaySweep

func ExecuteOverlaySweep(ctx context.Context, overlayPath string, batch SweepBatch, opts ...SweepOption) SweepReport

ExecuteOverlaySweep runs an approved batch and returns what happened.

Failure isolation

A directory's error is recorded on its own result and never returned as the batch's error. One unreadable directory, one locked file or one failing pkgdev must not strand the other ninety-nine — the sweep's whole purpose is clearing an accumulation, and an all-or-nothing batch would make the accumulation permanent.

Why the default is serial

concurrency defaults to 1. Parallel `pkgdev manifest` runs are unproven against a repository-level lock or the metadata cache, so a caller that forgets to pass a bound gets the safe behaviour rather than the fast one.

type UpdateStatus

type UpdateStatus string

UpdateStatus represents the status of a pending update.

const (
	// StatusPending indicates the update has been detected but not yet applied
	StatusPending UpdateStatus = "pending"
	// StatusValidated indicates the update was applied and manifest succeeded
	StatusValidated UpdateStatus = "validated"
	// StatusFailed indicates the update application failed
	StatusFailed UpdateStatus = "failed"
	// StatusApplied indicates the update has been fully applied
	StatusApplied UpdateStatus = "applied"
)

Update status constants

func ValidStatuses

func ValidStatuses() []UpdateStatus

ValidStatuses returns all valid update statuses

type VendorOutcome added in v0.36.0

type VendorOutcome struct {
	Generated bool   // a tarball was produced in this run
	Tarball   string // path of the tarball inside the staged distdir
	File, URI string // the vendor distfile name and the URI it is served from
	SHA512    string // hex digest of the tarball, computed once after Generate
	// ProbeFailed reports a vendor probe that could not answer (a transport
	// error, a timeout or a 5xx): whether the tarball is served is unknown, so
	// the manifest failure that follows is the vendor step's, not the ebuild's.
	ProbeFailed bool
}

VendorOutcome is what the vendor step did for one staged candidate. The zero value means it produced nothing.

Directories

Path Synopsis
Package ebuilds reads the ebuild tree of an overlay: it locates a package directory, lists its ebuilds, selects the current ebuild for a slot or a version series, splits package atoms, labels and slots, and extracts EbuildMetadata from an ebuild file.
Package ebuilds reads the ebuild tree of an overlay: it locates a package directory, lists its ebuilds, selects the current ebuild for a slot or a version series, splits package atoms, labels and slots, and extracts EbuildMetadata from an ebuild file.
Package fetch is autoupdate's HTTP layer: the retrying client with its circuit breaker, per-host policies and body caps, the rate limiter, the response and fetch caches, header allow-listing with credential scoping, and authenticated distfile fetching driven by a record's meta.fetch_* keys, including the spec parser that --lint reuses.
Package fetch is autoupdate's HTTP layer: the retrying client with its circuit breaker, per-host policies and body caps, the rate limiter, the response and fetch caches, header allow-listing with credential scoping, and authenticated distfile fetching driven by a record's meta.fetch_* keys, including the spec parser that --lint reuses.
Package fixer holds autoupdate's agent fixers: LLM agents, run through the Claude Code CLI with the least-privilege profile from llm, that repair a failed bump — the manifest fixer, the build fixer, the bump reviewer and the registry fixer.
Package fixer holds autoupdate's agent fixers: LLM agents, run through the Claude Code CLI with the least-privilege profile from llm, that repair a failed bump — the manifest fixer, the build fixer, the bump reviewer and the registry fixer.
Package jsonpath parses the path syntax the autoupdate configuration uses to point at a value inside a JSON document, such as "tag_name", "[0].sha" or "versions[0].version".
Package jsonpath parses the path syntax the autoupdate configuration uses to point at a value inside a JSON document, such as "tag_name", "[0].sha" or "versions[0].version".
Package llm holds autoupdate's LLM providers: the OpenAI and Ollama HTTP clients and the Claude Code client that drives the local `claude` CLI, the least-privilege permission profile every agent run is given, and the classification of how a `claude` run ended.
Package llm holds autoupdate's LLM providers: the OpenAI and Ollama HTTP clients and the Claude Code client that drives the local `claude` CLI, the least-privilege permission profile every agent run is given, and the classification of how a `claude` run ended.
Package parse extracts versions from upstream content: JSON paths, regular expressions and HTML/XPath, version transforms and series selection, version-history extraction, and the validation of an extracted version against a registry record.
Package parse extracts versions from upstream content: JSON paths, regular expressions and HTML/XPath, version transforms and series selection, version-history extraction, and the validation of an extracted version against a registry record.
Package registry is the packages.toml registry: it loads and saves the file, validates each record (including its meta.fetch_* spec, judged by the fetch package's own parser), lints and repairs it, renders a record, prunes and removes records, and migrates auto-disabled entries.
Package registry is the packages.toml registry: it loads and saves the file, validates each record (including its meta.fetch_* spec, judged by the fetch package's own parser), lints and repairs it, renders a record, prunes and removes records, and migrates auto-disabled entries.
Package repair holds what an operator-present repair of a failed autoupdate bump needs: the repair bundle written beside a kept staged tree (its schema, its redacted failure text, its atomic write), the lookup that finds it again by package, and the argv of the interactive claude session seeded with it.
Package repair holds what an operator-present repair of a failed autoupdate bump needs: the repair bundle written beside a kept staged tree (its schema, its redacted failure text, its atomic write), the lookup that finds it again by package, and the argv of the interactive claude session seeded with it.
Package statefile reads, merges and saves autoupdate's JSON state files under the inter-process file lock, so concurrent bentoo processes never lose each other's writes.
Package statefile reads, merges and saves autoupdate's JSON state files under the inter-process file lock, so concurrent bentoo processes never lose each other's writes.
Package validate answers one question about an ebuild without building it: does it still match the source it points at?
Package validate answers one question about an ebuild without building it: does it still match the source it points at?
Package vendor handles the vendor tarball of a staged Go candidate ebuild.
Package vendor handles the vendor tarball of a staged Go candidate ebuild.

Jump to

Keyboard shortcuts

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