distill

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Sep 13, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

Documentation

Overview

Package distill turns the raw review findings behind lessons into candidate groups for LLM distillation (RFC-001 §5.4 Tier 2). Exact clustering cannot merge ten differently-worded findings about one mistake; a reader of the raw material can — this package prepares that reader's batches.

The pipeline is built around one economic invariant: an unchanged evidence set is never distilled twice. Findings carry stable ids (GitHub comment ids), a group's Signature hashes its member ids, and the store remembers which signatures were already processed. A new finding changes its group's signature — exactly that group is re-read, nothing else.

Grouping is a Grouper: the contract (deterministic output, stable signatures) is the architecture, the strategy is replaceable. The built-in lexical grouper connects findings that share salient tokens — identifiers survive in finding bodies, and "reset"/"pooled"/"Free" recurring across files is precisely the trace a semantic pattern leaves — then buckets the remainder by directory. An embedding-based grouper can replace it without touching the pipeline or the stored state.

Index

Constants

View Source
const (
	// TriggerPromptVersion is persisted with an answered trigger question.
	// Version 3 bounds evidence-path metadata; version 2 permits a directly
	// cited companion path, while version 1 told the model to omit every
	// trigger that was also an evidence path.
	TriggerPromptVersion = 3
)

Variables

This section is empty.

Functions

func ApplyPins

func ApplyPins(root string, ps []model.Proposal) error

ApplyPins inserts the proposals as pins into <root>/.seamark/ lessons.yaml, preserving everything already there: entries are inserted directly under an existing bare `pin:` line (list items at the head of the list — hand-written entries and their comments are untouched), or a new pin section is appended when none exists. The result must parse as a lessons config before one byte is written; a file this function cannot safely edit (a flow-style `pin: []`, say) is an error, and the caller falls back to printing the block.

func AuditScopes added in v0.4.0

func AuditScopes(st *store.Store, cfg *reviews.Config, root string,
	ps []model.Proposal, meta map[int64]model.Finding,
) (map[int64]ScopeAdvisory, error)

AuditScopes runs the trigger-scope check for a set of proposals. Pending rows use their stored note and regions. Applied rows use the live yaml pin's note — a reworded note is what fires — and rows whose pin is pruned or re-scoped by hand are skipped. Only living findings feed the check. Keyed by proposal ID; unflagged proposals are absent.

func Clusters

func Clusters(ps []model.Proposal) [][]model.Proposal

Clusters groups patterns that restate each other, largest first; singletons are omitted. It audits an existing pin file for guidance repeated in different words. Seamark reports the duplicates but never edits lessons.yaml without an explicit request.

func CountEvents

func CountEvents(cited []model.Finding) int

CountEvents re-exports model.CountEvents — the recurrence bar counts events, and older callers reach it through the distill package.

func CoverageRegions added in v0.2.0

func CoverageRegions(cited []model.Finding) []string

CoverageRegions computes a proposal's region set from its cited evidence: a small set of directories (≤ maxRegions, depth ≤ maxRegionDepth) covering at least coverTarget of the voting events, chosen greedily — most new events first. Greedy is not guaranteed minimal in general, but under this need/cap geometry no input has been found where it misses a cover an exhaustive search would find, and it stays linear in candidates where exhaustion is combinatorial. Nil means repo-wide — rendered `*`, exactly as before.

func RenderPins

func RenderPins(ps []model.Proposal) (string, error)

RenderPins renders proposals as ready-to-paste pin entries, each with its provenance comment. The YAML values are marshaled, not hand-quoted — a note containing quotes or colons must not be able to corrupt the file.

Types

type Config

type Config struct {
	Distill struct {
		// Write lets `lessons --apply` edit .seamark/lessons.yaml. Off
		// by default: without it, apply prints the pin block for manual
		// insertion. Seamark edits a reviewed config only when that same
		// config opts in.
		Write bool `yaml:"write"`
	} `yaml:"distill"`
}

Config is the distill section of .seamark/config.yaml.

func LoadConfig

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

LoadConfig reads the distill section of the shared config file. Same contract as the other section readers: absent file means defaults, malformed file is loud.

type ExtractOptions added in v0.4.0

type ExtractOptions struct {
	// Root is the workspace root; named paths are verified against it.
	Root string
	// DryRun stops after the preflight: nothing is sent.
	DryRun bool
	// Agent is the resolved agent command line, for disclosure only.
	Agent []string
	// OnPreflight receives the plan before the first call — always,
	// so sending note text to a model-backed CLI is never a surprise.
	OnPreflight func(ExtractPreflight)
	// OnBatchStart/OnBatchDone drive interactive surfaces through the
	// long silent stretch of each agent call — the same contract as
	// distill's group callbacks. When nil, the same information flows
	// through Logf as plain lines.
	OnBatchStart func(desc string)
	OnBatchDone  func(outcome string)
	// Logf receives progress; nil discards it.
	Logf func(format string, args ...any)
}

ExtractOptions configures ExtractTriggers.

type ExtractPreflight added in v0.4.0

type ExtractPreflight struct {
	Proposals   int
	Batches     int
	PromptChars int
	Agent       []string
	BodyCap     int
}

ExtractPreflight is the disclosure: what would be sent, where, and roughly what it costs.

func (ExtractPreflight) Tokens added in v0.4.0

func (p ExtractPreflight) Tokens() string

Tokens estimates the preflight's prompt volume.

type ExtractResult added in v0.4.0

type ExtractResult struct {
	Examined   int // proposals sent to the agent
	Named      int // proposals whose reply named at least one path
	Stored     int // proposals with at least one validated path stored
	Retargeted int // pending rows whose regions changed
	// AppliedStored counts applied pins that stored new triggers —
	// the rows whose drift the user must apply with --retarget. An
	// explicit counter: inferring it from Stored minus Retargeted
	// miscounts pending rows whose regions did not change.
	AppliedStored int
	BatchesFailed int // batches lost to agent or parse errors (retryable)
	PromptChars   int
	ReplyChars    int
	Duration      time.Duration
}

ExtractResult reports what one extraction run did.

func ExtractTriggers added in v0.4.0

func ExtractTriggers(ctx context.Context, st *store.Store, inv agent.Invoker,
	ps []model.Proposal, meta map[int64]model.Finding, opts ExtractOptions,
) (*ExtractResult, error)

ExtractTriggers runs the backfill over the given proposals. The caller selects them (the idempotency filter — rows already carrying triggers — belongs there) and provides the living-findings map the other surfaces already hold. Batches fail independently: a lost batch is logged and retried on the next run, like a distill group. On a store error the result rides along with it — the counters name what completed before the failure, and stamped rows stay done.

func (ExtractResult) TokensBack added in v0.4.0

func (r ExtractResult) TokensBack() string

TokensBack estimates the run's reply traffic for the summary line.

func (ExtractResult) TokensSent added in v0.4.0

func (r ExtractResult) TokensSent() string

TokensSent estimates the run's prompt traffic for the summary line.

type FindingPlan added in v0.5.0

type FindingPlan struct {
	ID     int64
	Source string
	PR     int
	Path   string
	Paths  []string
}

FindingPlan identifies one finding without disclosing its body. It lets a user verify the evidence boundary before approving a model call.

type Group

type Group struct {
	// Region is the deepest directory common to every member, "" when
	// the members span top-level trees (a repo-wide theme batch).
	Region string
	// Findings are the members, ordered by id.
	Findings []model.Finding
	// Signature identifies the evidence set: the hash of the member
	// ids. Same members — same signature, regardless of ordering or of
	// anything outside the group.
	Signature string
	// Area marks a directory bucket: thematically unconnected findings
	// batched so distillation still covers them. Membership in an area
	// group only means "same directory", so consumers that treat group
	// membership as "same mistake" (the outcome loop) must skip these.
	Area bool
}

Group is one distillation batch: findings that plausibly share a theme (or at least a neighborhood), with a stable identity.

type GroupPlan

type GroupPlan struct {
	Signature   string
	Region      string
	Findings    int
	PromptChars int
	BodyCap     int
	Evidence    []FindingPlan
}

GroupPlan is one group's slice of the preflight.

type Grouper

type Grouper interface {
	Group(findings []model.Finding) []Group
}

Grouper buckets findings into candidate groups. Implementations must be deterministic: the same findings yield the same groups with the same signatures, in the same order.

func NewLexicalGrouper

func NewLexicalGrouper() Grouper

NewLexicalGrouper returns the default token-overlap Grouper.

Three shared tokens form an unconstrained strong edge. Two-token edges are useful wording bridges (the pooled-state corpus needs them), but may only build a small component. This two-tier rule prevents the weak transitive chains observed in large public repositories without giving up the recall that motivated lexical grouping.

type Known

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

Known is the set of patterns already captured: every pin in lessons.yaml (hand-written ones included) and every proposal already decided or pending. A distilled pattern matching one of these is not news — including one the user dismissed, since a dismissal is a decision the distiller must not relitigate.

func NewKnown

func NewKnown(patterns ...[]model.Proposal) *Known

NewKnown builds the set. Pins arrive as Proposals carrying just Rule and Note, so config pins and stored proposals share one shape.

func (*Known) Add

func (k *Known) Add(p model.Proposal)

Add records a pattern as known — used as a run proceeds, so two groups in the same pass cannot both propose the same thing.

func (*Known) Labels

func (k *Known) Labels(region string, limit int) []string

Labels lists the rule names already captured for an area, newest first, at most limit of them. They are fed to the distiller so it can skip what is known and spend the call looking for something else — labels only, because the notes would cost more tokens than the duplicates they prevent.

func (*Known) Restated

func (k *Known) Restated(p model.Proposal) (string, bool)

Restated returns the label of the known pattern p duplicates, and whether it duplicates one at all. Evidence identity is checked before wording: it is the stronger claim, and it catches the pairs wording cannot.

Identity deliberately skips patterns from p's own reply batch (same signature): one finding can flag two mistakes, so a model reading a group may honestly cite the same members for distinct patterns — the wording check still guards that batch against padding. Across batches the same citations mean the corpus was re-carved and the theme re-derived, which is exactly the duplication to stop.

type Options

type Options struct {
	// Region restricts the finding corpus before grouping ("" =
	// everywhere). This keeps unrelated findings outside a requested tree
	// from changing token frequencies, bridging components, or consuming a
	// budgeted call.
	Region string
	// Limit caps how many new groups one run reads (0 = all). The cap
	// is the budget lever: each group is one agent invocation.
	Limit int
	// Logf receives progress; nil discards it.
	Logf func(format string, args ...any)
	// OnGroupStart/OnGroupDone drive interactive surfaces through the
	// long silent stretch of each agent call: start fires with the
	// group's description before the call, done with the outcome after.
	// When nil, the same information flows through Logf as plain lines.
	OnGroupStart func(desc string)
	OnGroupDone  func(outcome string)
	// Pins are the patterns already captured in .seamark/lessons.yaml,
	// hand-written ones included, as Rule/Note pairs. A distilled
	// pattern that restates one of them is dropped: groups are read
	// independently, so a repo-wide mistake would otherwise be
	// re-proposed under a new name by every group it appears in.
	Pins []model.Proposal
	// Agent is the resolved agent command line, for the preflight
	// disclosure only — the Invoker is what actually runs it.
	Agent []string
	// Root is the workspace root. Trigger paths named by the agent are
	// verified against this tree; empty drops them all — an unverified
	// name must not reach storage.
	Root string
	// OnPreflight receives the plan before the first agent call: what
	// would be sent, where, and roughly what it would cost.
	OnPreflight func(Preflight)
	// DryRun stops after the preflight: nothing is sent, marked, pruned,
	// or persisted. The Invoker may be nil on a dry run.
	DryRun bool
}

Options tunes one distillation run.

type PinKey

type PinKey = reviews.PinKey

PinKey aliases the pin identity owned by the reviews package — apply and prune consume it, but the identity of a pin belongs with PinRule, and surfaces (report) must reach it without importing the distiller.

func NewPinKey added in v0.2.0

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

NewPinKey re-exports reviews.NewPinKey for the apply/prune callers.

func RemovePins

func RemovePins(root string, keys []PinKey) (removed []PinKey, err error)

RemovePins deletes the named pins from <root>/.seamark/lessons.yaml, with their provenance comments, leaving every other byte alone. It is the inverse of ApplyPins and deliberately more careful: adding a wrong entry is visible in a diff, while deleting a neighbouring one destroys work. A pin already absent is not an error (the user may have pruned it by hand); the result must parse AND contain exactly the expected remaining pins, or nothing is written.

type Preflight

type Preflight struct {
	// Agent is the command line each group's prompt would be piped to.
	Agent []string
	// Groups describes each evidence group that would be sent this run.
	Groups []GroupPlan
	// PromptChars totals the prompts exactly as they would be sent now.
	// Execution primes later prompts with labels learned from earlier
	// groups, so treat it as a close lower bound.
	PromptChars int
	// Findings counts the finding bodies across all planned groups.
	Findings int
	// BodyCap is the largest per-finding evidence cap across planned groups.
	// Groups share a fixed total budget, so smaller groups can show more
	// of each finding. Path metadata and body share this cap; dispatch
	// reapplies best-effort secret redaction before the body is sent.
	BodyCap int
}

Preflight is the pre-invocation disclosure. Metadata only — never finding bodies.

func (Preflight) Tokens

func (p Preflight) Tokens() string

Tokens renders the preflight's approximate token cost.

type Result

type Result struct {
	GroupsTotal   int // candidate groups in the current corpus
	GroupsSkipped int // already distilled (signature known)
	GroupsRead    int // sent to the agent this run
	GroupsFailed  int // agent or parse errors (not marked; retried next run)
	GroupsPending int // new groups left unread (limit or region filter)
	PrunedStale   int // pending proposals dropped because their group changed
	// PromptChars/ReplyChars meter the run's agent traffic — the basis
	// for the ~token estimate shown to the user. Failed groups count
	// too: their cost was paid.
	PromptChars int
	ReplyChars  int
	Duration    time.Duration
	// Duplicates counts distilled patterns dropped as restatements of
	// something already pinned, proposed, or decided.
	Duplicates int
	Proposals  []model.Proposal
}

Result reports what a run did.

func Run

func Run(ctx context.Context, st *store.Store, grouper Grouper, inv agent.Invoker, opts Options) (*Result, error)

Run executes the plan half of distillation: group the findings, skip evidence sets already read, send each remaining group to the agent, validate what comes back, and persist the survivors as proposals. It never touches .seamark/lessons.yaml — applying is a separate, operator decision.

func (*Result) CostNote

func (r *Result) CostNote() string

CostNote renders what the run cost: estimated tokens both ways and wall time. Empty when no agent traffic happened — a fully-skipped run was free and says nothing.

type ScopeAdvisory added in v0.4.0

type ScopeAdvisory struct {
	// NotePath is the path the note names, outside the regions. It
	// exists in the working tree — a missing path is never a target.
	NotePath string

	// Partner is the co-change partner file that agrees with
	// NotePath: equal to it, or under it when NotePath is a
	// directory. Co-change names files; notes often name directories.
	Partner string

	// Evidence is the cited finding file whose partner agreed.
	Evidence string

	// Together counts the shared commits on the Evidence–Partner
	// edge, so the printed advisory can show its strength.
	Together int

	// Suggested is the pin's current region set with the trigger
	// region appended. Regions the trigger region contains leave
	// first: delivery regions form a union, so a contained region
	// adds nothing and wastes a cap slot. Survivors keep their order
	// — order only feeds the legacy region: field; pin identity
	// sorts regions. Nil when the result would still pass
	// maxRegions: the user decides what to remove.
	Suggested []string
}

ScopeAdvisory says that a pin's delivery regions likely miss the trigger site. Two signals must agree on the same place: the note names a repo path outside the regions, and the co-change history of a cited evidence file points at it too. One signal alone is not enough.

func AuditScope added in v0.4.0

func AuditScope(st *store.Store, root, note string, regions []string, cited []model.Finding) (ScopeAdvisory, bool, error)

AuditScope runs the trigger-scope check for one pin. regions must be the pin's live region set; nil means repo-wide, which already delivers everywhere, so the check reports nothing. When several partner edges agree, the one with the most shared commits wins. ok is false when the signals do not agree — the common case.

func (ScopeAdvisory) Line added in v0.4.0

func (a ScopeAdvisory) Line() string

Line renders the advisory sentence

type TriggerFact added in v0.4.0

type TriggerFact struct {
	Path     string // the stored trigger path
	Region   string // its exact delivery region; "" for vanished paths
	Together int    // strongest confirming co-change edge; 0 = unconfirmed
	Direct   bool   // true when the trigger is itself cited evidence
	Selected bool   // true when the recomputed delivery set reaches it
}

TriggerFact reports one stored trigger path's state under today's history, for the plan and ledger surfaces.

func RecomputeRegions added in v0.4.0

func RecomputeRegions(st *store.Store, root string, p model.Proposal, living []model.Finding,
) ([]string, []TriggerFact, error)

RecomputeRegions returns the regions today's inference assigns a proposal. Verified trigger paths are the most precise delivery surface: a trigger is accepted when it is directly cited evidence or when history confirms it co-changes with that evidence. If none are accepted, evidence coverage is the conservative fallback. Every reader of "regions now" — distillation, the ledger, --retarget — must use this one function. root locates the working tree and rejects vanished trigger paths.

func (TriggerFact) BlockedLine added in v0.4.0

func (f TriggerFact) BlockedLine() string

BlockedLine renders the confirmed-but-undeliverable sentence every surface prints — the plan, the ledger, and the HTML report must not phrase the same fact differently. Empty for selected or unconfirmed facts.

Jump to

Keyboard shortcuts

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