factcache

package
v1.5.0 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package factcache is the extractor-fact cache adapter: content-addressed JSON blobs under .archfit-cache/facts/<analyzer>/<key>.json that let a warm run skip expensive out-of-process extractor work (docs/design/fact-cache.md). It stores FACTS — the subprocess output extractors derive facts from — never scores: classification and scoring re-run every time, so config and ScoreVersion changes take effect instantly with no invalidation logic.

The cache is best-effort by contract: a corrupted or unreadable entry is a miss (logged at debug), a failed write never fails the run, and a nil *Store disables reads AND writes (the --no-cache path — a bypassed run must not poison or refresh entries).

Index

Constants

View Source
const (
	// MaxBytes caps the total size of the facts tree (fact-cache.md D4);
	// sized for SCIP index blobs, the largest fact class.
	MaxBytes int64 = 1 << 30 // 1 GiB
	// EvictToBytes is the eviction target (75% of MaxBytes), so eviction
	// runs in bursts instead of on every write at the boundary.
	EvictToBytes int64 = 768 << 20 // 768 MiB

)

Variables

This section is empty.

Functions

func HashJSON

func HashJSON(v any) (string, error)

HashJSON returns the sha256 hex of v's JSON encoding. encoding/json sorts map keys, so the encoding — and the hash — is deterministic. Used for the config-slice component of Key: hash the typed view the analyzer consumes, not the whole config, so an unrelated config edit does not invalidate facts.

func HashTree

func HashTree(root string, relPaths []string) (string, error)

HashTree returns the input-tree hash for Key: sha256 over the sorted (relpath, content-hash) pairs of relPaths under root. Content hashes, not mtimes — mtime invalidation breaks under git checkout/rebase, and hashing a whole repo is sub-second next to the subprocess runs the cache guards. Enumeration order does not matter; a missing or unreadable file is an error and the caller falls back to an uncached run.

func Key

func Key(analyzer, toolVersion, configSliceHash, inputTreeHash string) string

Key derives the cache key for one analyzer invocation scope (fact-cache.md D3): sha256 over the schema version, analyzer name, tool version, config-slice hash, and input-tree hash, NUL-separated to prevent boundary ambiguity. Any component change ⇒ new key ⇒ miss — stale entries are never served, only orphaned (eviction reclaims them).

func ListInputs

func ListInputs(root string, match func(rel string) bool, exclude []string) []string

ListInputs walks root and returns the sorted slash-relative paths of the regular files that form one analyzer's input scope: match(rel) selects by path (extension, basename, …) and exclude globs (doublestar, matched against the slash-relative path) drop the rest. Feed the result to HashTree for the key's input-tree component. The walk is best-effort: unreadable entries are skipped, never an error — a file that vanishes between walk and hash is HashTree's error to report.

func MatchAll

func MatchAll(string) bool

MatchAll selects every regular file — for analyzers whose input scope is the whole tree (jscpd, ast-grep).

func MatchExts

func MatchExts(exts []string, basenames []string) func(rel string) bool

MatchExts returns a match func for ListInputs that selects files by extension (e.g. ".go") or exact basename (e.g. "go.mod", "tsconfig.json").

Types

type Runner

type Runner struct {
	// Inner executes on a cache miss. Required.
	Inner toolrun.Runner
	// Store holds the fact blobs. nil disables the cache entirely — the
	// --no-cache path: no reads, no writes.
	Store *Store
	// Analyzer is the store subdirectory this runner writes under.
	Analyzer string
	// Key is the invocation-scope cache key from Key() — tool version,
	// config-slice hash, and input-tree hash already folded in.
	Key string
	// Cacheable, when non-nil, vetoes storing a completed result (e.g. an
	// extractor refusing to cache output it would report as partial —
	// fact-cache.md D3). nil caches every exec-level success, any exit
	// code: the extractor, not the raw exit code, decides what a fact is.
	Cacheable func(toolrun.Output) bool
	// EntryArgs, when non-nil, replaces cmd Name+Args as the entry-address
	// material — for commands whose argv embeds a nondeterministic temp path
	// (e.g. the py grimp helper file). The caller must pick EntryArgs so that
	// Key+EntryArgs still uniquely identify the invocation's inputs.
	EntryArgs []string
}

Runner is a caching decorator around toolrun.Runner (fact-cache.md D5, seam 1). It serves Run/Stream results from the fact store on a key hit and records them on a miss. Construct one Runner per analyzer invocation scope, immediately before the expensive subprocess call — never around version probes: probes are key material, not facts, and are never cached.

Entries are addressed by Key plus a digest of the command name and args. WorkDir and Env are deliberately excluded: a --base temp worktree gets a fresh absolute path every run, and content identity is already pinned by the key's input-tree hash. Corollary: commands that differ ONLY by WorkDir or Env MUST use distinct Keys, and args must stay deterministic (repo-relative, no temp paths) for entries to survive across runs.

func (*Runner) Detect

func (r *Runner) Detect(ctx context.Context, tool string) (toolrun.ToolInfo, bool)

Detect delegates to Inner — tool presence probes are never cached.

func (*Runner) Run

func (r *Runner) Run(ctx context.Context, cmd toolrun.ToolCmd) (toolrun.Output, error)

Run returns the cached Output on a hit; on a miss it executes via Inner and stores the result. Exec-level failures and timeouts (err != nil) are transient and never cached; non-zero exits are cached — the extractor decides whether that output is a usable fact.

func (*Runner) Stream

func (r *Runner) Stream(ctx context.Context, cmd toolrun.ToolCmd, consume func(io.Reader) error) (toolrun.Output, error)

Stream serves the cached stdout to consume on a hit; on a miss it tees the live stream into the cache. The toolrun contract requires consume to drain stdout to EOF on success — bytes it leaves unread are drained by the inner runner and would be missing from the cached entry, so that contract is what keeps cached and live streams identical.

type Store

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

Store is the content-addressed fact-blob store rooted at .archfit-cache/facts. A nil *Store is valid and disables the cache: Get always misses and Put is a no-op.

func NewStore

func NewStore(dir string) *Store

NewStore returns a Store rooted at dir (e.g. <configDir>/.archfit-cache/facts) with the default size cap. The directory is created lazily on first Put.

func (*Store) Get

func (s *Store) Get(analyzer, key string) ([]byte, bool)

Get returns the blob stored under analyzer/key. Any miss — absent entry, unreadable file, nil store — is (nil, false); the caller re-derives. A hit touches the entry mtime (best-effort) so LRU eviction spares it.

func (*Store) Put

func (s *Store) Put(analyzer, key string, data []byte)

Put stores data under analyzer/key atomically (tmp file + rename in the same directory — same key ⇒ same content, so concurrent last-writer-wins is safe). Best-effort: any failure is logged at debug and never fails the run. A successful write triggers the size-cap eviction pass.

Jump to

Keyboard shortcuts

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