reviews

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package reviews mines pull-request review comments into lessons: the recurring feedback a repository's reviewers — CodeRabbit, Copilot, or humans — leave on the same kind of change. It is the capture half of RFC-001 §5.4's anti-repeat loop, and the source is reviewer-agnostic: every reviewer's comments arrive through the same GitHub API, so a bot and a human are clustered the same way.

The network is reached through an injectable Fetcher; the default shells out to `gh`, and every failure mode (no gh, no auth, no GitHub remote, no PRs) degrades to an empty result with a note, never an error that aborts indexing.

Index

Constants

View Source
const DefaultChangeBudget = 6

DefaultChangeBudget bounds the lessons one change_set answer carries. Its own knob, not the hook's: a ten-file change set under the per-file budget would print thirty lines.

View Source
const DefaultPinBudget = 3

DefaultPinBudget bounds how many pins one ambient injection (the edit hook) carries. Distillation made pins cheap to create, and broad regions accumulate: without a budget, every applied pin taxes every future edit. Deliberate views (--file, why) stay unbudgeted — asking is consent; being injected into is not.

View Source
const DefaultReviewWindowDays = 730

DefaultReviewWindowDays is the review-mining recency window — twice the fix window, because review prose ages better than patches.

View Source
const DefaultThreshold = 2

DefaultThreshold is the minimum recurrences before a mined lesson surfaces: a single comment is noise, a pattern needs repetition.

Variables

This section is empty.

Functions

func RecordFiring

func RecordFiring(root, file, tool string, lessons []model.Lesson) error

RecordFiring is RecordFiringSurface for the edit hook, the original (and unnamed) surface.

func RecordFiringSurface added in v0.2.0

func RecordFiringSurface(root, surface string, files []string, tool string, lessons []model.Lesson) error

RecordFiringSurface appends a firing record to <root>/.seamark/lessons-audit.jsonl with the emitting surface named ("" reads as the edit hook | change_set | check) — --stats reads all ambient exposure, not just the hook's. Files travel individually so a ten-file check counts ten files, not one joined string. Callers treat the error as best-effort: an audit write must never fail the action it observed.

func RegionMatches

func RegionMatches(region, target string) bool

RegionMatches reports whether target sits within region: an exact match, or region as a path-prefix directory of target. Exported for the `lessons --region` ledger filter.

Types

type Comment

type Comment struct {
	ID        int64
	Reviewer  string // classified: coderabbit | copilot | bot | human
	Author    string // raw user.login
	Body      string
	Path      string // repo-relative file the comment lands on
	Line      int    // resolved line (line, else original_line)
	URL       string // html_url, for provenance
	CreatedAt int64  // unix seconds
	PR        int    // pull-request number
	RuleCode  string // extracted linter code (RUF001, reportArgumentType…), or ""
	InReplyTo int64  // id of the thread's top comment; 0 when this IS one
}

Comment is one review comment, normalized from the GitHub API.

type Config

type Config struct {
	// Threshold overrides the minimum recurrences to surface a lesson.
	Threshold int `yaml:"threshold"`
	// PinBudget overrides how many pins the edit hook injects per edit
	// (most-specific regions first; the rest are one pointer line away).
	// 0 means DefaultPinBudget.
	PinBudget int `yaml:"pin_budget"`
	// ChangeBudget overrides how many lessons one change_set answer
	// carries across all its files. 0 means DefaultChangeBudget.
	ChangeBudget int `yaml:"change_budget"`
	// Mute hides mined lessons by rule code and/or region prefix.
	Mute []MuteRule `yaml:"mute"`
	// Pin surfaces curated lessons unconditionally — the "must not be
	// ignored" list — even when mining never found them.
	Pin []PinRule `yaml:"pin"`
}

Config tunes how mined lessons surface (`.seamark/lessons.yaml`), applied at surface time so edits take effect without re-mining — the same contract as the gate's policy.yaml. An absent file yields defaults: nothing muted, nothing pinned, threshold DefaultThreshold.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig is the zero-tuning config: nothing muted or pinned, default threshold. Used as the fallback when a config file is absent or unreadable.

func LoadConfig

func LoadConfig(root string) (*Config, error)

LoadConfig reads <root>/.seamark/lessons.yaml. A missing file is not an error — it means "defaults". A malformed file IS an error: silently ignoring a typo'd mute would surface noise the user asked to hide. Callers that must stay robust (why/orient/MCP) fall back to DefaultConfig on error rather than failing the whole report.

func (*Config) ChangeSetBudget added in v0.2.0

func (c *Config) ChangeSetBudget() int

ChangeSetBudget resolves the effective change_set lesson cap.

func (*Config) HookPinBudget

func (c *Config) HookPinBudget() int

HookPinBudget resolves the effective per-injection pin cap.

func (*Config) Muted

func (c *Config) Muted(l model.Lesson) bool

Muted reports whether a mined lesson is hidden by config — exported so the `--list` ledger can flag what the user has already silenced.

func (*Config) Surface

func (c *Config) Surface(mined []model.Lesson, scope string) []model.Lesson

Surface applies the config to a set of mined lessons for a given scope: drop muted lessons, drop those below threshold, then prepend the applicable pins. The result is what any surface (why, orient, the hook) should show.

func (*Config) SurfaceBudget

func (c *Config) SurfaceBudget(mined []model.Lesson, scope string, pinBudget int) (out []model.Lesson, trimmed int)

SurfaceBudget is Surface with a pin cap for ambient surfaces: at most pinBudget pins (0 = unlimited), most specific region first — a pin on the file beats one on its package beats a repo-wide `*`. The trimmed count comes back so the caller can say "+N more" instead of hiding them: budgeted, never silent.

func (*Config) SurfaceBudgetAnnotated added in v0.2.0

func (c *Config) SurfaceBudgetAnnotated(mined []model.Lesson, scope string, pinBudget int,
	annotate PinAnnotator,
) (out []model.Lesson, trimmed int)

SurfaceBudgetAnnotated is SurfaceBudget with a PinAnnotator: pins compete for budget slots by (confidence rank, deepest matching region) — a weak-evidence pin must not hold a slot a strong one wants — and annotator notes travel into the rendered lesson.

func (*Config) SurfacePins added in v0.2.0

func (c *Config) SurfacePins(scope string, annotate PinAnnotator) []SurfacedPin

SurfacePins returns the pins applying to scope, ordered by (confidence rank, specificity, file order) — restatements NOT collapsed, so a caller merging several scopes can collapse once at its own grain.

func (*Config) SurfacesMined added in v0.2.0

func (c *Config) SurfacesMined(l model.Lesson) bool

SurfacesMined reports whether a mined lesson clears the surfacing bar: recurred at least Threshold times and not muted. The one rule every surface (hook, why, change_set, check) applies — inlining it at a call site invites the surfaces to quietly disagree.

type Fetcher

type Fetcher func(owner, repo string, opts Options) ([]byte, error)

Fetcher returns raw GitHub review-comment JSON (a single JSON array, paginated pages already concatenated) for owner/repo. Injected so tests never touch the network.

type Fired

type Fired struct {
	Region  string
	Symptom string
	Count   int
	LastTS  string
}

Fired is one lesson's firing tally.

type FiredLesson

type FiredLesson struct {
	Region  string `json:"region"`
	Symptom string `json:"symptom"`
}

FiredLesson identifies one lesson surfaced in a firing.

type Firing

type Firing struct {
	TS string `json:"ts"`
	// Surface names the emitter: "" reads as the edit hook (the only
	// writer before change_set and check joined).
	Surface string `json:"surface,omitempty"`
	// File carries a single-file firing (the hook); Files a multi-file
	// one (change_set, check) — individually, so distinct-file counts
	// stay meaningful whatever order a diff lists them in.
	File  string        `json:"file,omitempty"`
	Files []string      `json:"files,omitempty"`
	Tool  string        `json:"tool,omitempty"`
	Fired []FiredLesson `json:"fired"`
}

Firing is one record of the edit hook surfacing lessons before an edit.

func ReadFirings

func ReadFirings(root string) ([]Firing, error)

ReadFirings loads the firing log. A missing log is empty history, not an error; an unparseable line is skipped so one corrupt append never hides the rest.

type MuteRule

type MuteRule struct {
	Rule   string `yaml:"rule"`
	Region string `yaml:"region"`
}

MuteRule hides lessons. An empty field matches anything, so {rule: F541} mutes that code everywhere and {region: alembic/versions} mutes every lesson under that path.

type Options

type Options struct {
	// Logf receives fetch and clustering progress; nil discards it. The
	// GitHub fetch is the silent long pole of a mine — pages of network
	// I/O — and silence reads as stuck.
	Logf func(format string, args ...any)
	// WindowDays bounds how old a review comment may be and still
	// enter the corpus (RFC-002 §9): fix mining always had a shelf
	// life; review comments were immortal, and seven-year-old feedback
	// weighed like last month's. 0 means DefaultReviewWindowDays;
	// negative means unlimited (the config's `window_days: 0`).
	WindowDays int
}

Options bounds a mining pass. (The field for a `since` watermark lands with incremental mining.)

type PinAnnotator added in v0.2.0

type PinAnnotator func(PinRule) (rank int, note string)

PinAnnotator supplies surface-time knowledge the config layer cannot compute itself: an evidence-confidence rank (higher surfaces first) and an optional note appended to the pin's rendered symptom. A nil annotator ranks every pin equally and adds nothing.

type PinKey added in v0.2.0

type PinKey struct {
	Rule   string
	Region string
}

PinKey identifies one pin entry. A rule name alone is not an identity: the same rule is legitimately pinned in several regions (RUF001 for scripts and for api), and pruning one must not take the others with it. Region holds the canonical form of the pin's whole region set — sorted, NUL-joined, "*" for repo-wide — so the key stays comparable, region ORDER never splits an identity, and no legal path byte can make two different sets collide (a comma can appear in a directory name; NUL cannot).

func NewPinKey added in v0.2.0

func NewPinKey(rule, region string, regions []string) PinKey

NewPinKey builds the canonical identity from a pin's rule and its region fields (single + set, either may be empty). Every consumer — apply, prune, the file parser, liveness checks — must construct keys through here, or two spellings of one pin drift apart.

func (PinKey) String added in v0.2.0

func (k PinKey) String() string

String renders the key for error messages and logs: the NUL joins become readable separators.

type PinRule

type PinRule struct {
	Rule   string `yaml:"rule"`   // the symptom shown (a code or short label)
	Region string `yaml:"region"` // file or directory; "*" or "" is repo-wide
	// Regions widens a pin to a small set of places — one theme
	// legitimately living in `api` AND `db` needs no repo-wide `*`.
	// Flow-rendered (`regions: [api, db]`) so applied entries stay
	// single-line-parseable; readers predating this field see Region,
	// the set's first entry — narrower than the `*` they used to get.
	Regions []string `yaml:"regions,omitempty,flow"`
	Note    string   `yaml:"note"` // human explanation, carried into output
}

PinRule is a hand-authored lesson that always surfaces for its region(s).

func (PinRule) AllRegions added in v0.2.0

func (p PinRule) AllRegions() []string

AllRegions returns the pin's effective region set — Region and Regions merged, trailing slashes trimmed, deduplicated. Nil means repo-wide ("*" and "" collapse to it).

type Result

type Result struct {
	Lessons  []model.Lesson
	Findings []model.Finding
	Fetched  bool
	Note     string
}

Result carries what one mining pass produced. Fetched distinguishes a genuine "zero comments now" (Fetched, empty Lessons — the set may be cleared) from a degraded run (not Fetched — the source was absent or unreachable, and stored lessons must be preserved). Note explains an empty result either way. Findings are the raw comments behind Lessons, stored alongside them so deeper passes work from full material.

func Mine

func Mine(root string, opts Options, fetch Fetcher) (Result, error)

Mine fetches review comments for the repository at root, parses and clusters them, and returns the resulting lessons. A nil fetch uses the gh-backed default. It never returns an error for an absent or unreachable source — that is a Note with Fetched=false, so callers know not to overwrite good data with the fruits of a failed fetch.

type Summary

type Summary struct {
	Total int // firing events across every surface
	// BySurface splits the events: an actual pre-edit hook reminder, a
	// change_set plan, and a CI check are different kinds of exposure,
	// and "edits reminded" must not count the other two.
	BySurface  map[string]int
	Files      int            // distinct files that triggered a firing
	Ranked     []Fired        // surfaced lessons that fired, most-fired first
	NeverFired []model.Lesson // lessons that would surface but never have
}

Summary is the aggregate the `--stats` view renders.

func Summarize

func Summarize(firings []Firing, surfaced []model.Lesson) Summary

Summarize aggregates firings and cross-references the currently- surfaced lessons to find those that never fire — the decay signal: a lesson whose region no edit has touched is a pruning candidate.

type SurfacedPin added in v0.2.0

type SurfacedPin struct {
	Pin  PinRule
	Rank int    // annotator's confidence rank; equal when unannotated
	Note string // annotator's display note, "" for none
	// Depth is the deepest of the pin's regions matching the queried
	// scope — the specificity that earned it consideration there.
	Depth int
}

SurfacedPin is one applicable pin with its surfacing metadata — the building block for surfaces that merge pins across scopes and must keep rank and specificity through the merge.

func CollapseRestated added in v0.2.0

func CollapseRestated(pins []SurfacedPin) (kept []SurfacedPin, dropped int)

CollapseRestated drops surfaced pins that restate an earlier — higher-ranked, per the caller's order — pin. Greedy against the kept set, not transitive: a pin is dropped only when it restates one that survived. The dropped count feeds the caller's "+N more" so a collapsed duplicate is pointed at, never silently hidden. (Bare linter-code pins collapse only on equal codes — wording.Topic owns that rule, so every dedup surface treats codes the same way.)

func (SurfacedPin) Lesson added in v0.2.0

func (sp SurfacedPin) Lesson() model.Lesson

Lesson renders the surfaced pin the way every surface shows pins.

Jump to

Keyboard shortcuts

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