curate

package
v0.11.0 Latest Latest
Warning

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

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

Documentation

Overview

Package curate is the Phase-2 grooming agent: it dedups the KB backlog, gates the decision-ready queue on incident resolution, surfaces recurring blind spots as knowledge-gap issues, and drives lifecycle/decay. It writes to the forge only — it never merges and never touches a human-labelled artifact.

Index

Constants

View Source
const DefaultSweepInterval = 6 * time.Hour

DefaultSweepInterval is the fallback cadence when no interval is configured. config.applyDefaults ships the same value; this guard only protects direct users.

Variables

This section is empty.

Functions

func TitleJaccard added in v0.11.0

func TitleJaccard(a, b string) float64

TitleJaccard scores two entry titles by Jaccard similarity over their noise-filtered token sets — the same fuzzy-duplicate primitive Dedup uses for markerless PRs, shared with `lore kb import` so "duplicate" means the same thing at import time and at curation time. ≥0.6 is the house threshold.

Types

type Agent

type Agent struct {
	Passes []Pass
	Log    *slog.Logger
}

Agent runs the grooming passes in order. Forge-only writes; never merges.

func (Agent) Run

func (a Agent) Run(ctx context.Context)

Run executes every pass, logging (not propagating) per-pass errors.

type ClosedPRLister added in v0.4.0

type ClosedPRLister interface {
	ListClosedUnmergedPRsByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)
}

ClosedPRLister lists closed-but-unmerged KB PRs carrying a label. Merged PRs are accepted entries (never suppressed) and are filtered out by the implementation.

type ClosedPRSuppression added in v0.4.0

type ClosedPRSuppression struct {
	Forge ClosedPRLister
}

ClosedPRSuppression derives the suppression set on every run from the forge's closed-unmerged KB PRs — no mutable store or watermark, mirroring Recurrence's idempotent, ledger-driven design. The forge retains closed PRs, and each carries the same hidden DupFingerprint marker the drafter stamped, so the set is reconstructable each pass. A markerless (legacy/hand-filed) close is skipped: there is no stable key to suppress or count it on.

func (ClosedPRSuppression) Suppressed added in v0.4.0

Suppressed implements SuppressionSource.

type Contested added in v0.8.0

type Contested struct {
	Forge  ContestedForge
	Ledger ContestedSource
	KBRepo string // configured forge.kb_repo ("owner/name"); KB links outside it are foreign and skipped
	Log    *slog.Logger
}

Contested surfaces standing 👎 votes on the OPEN KB PR they relate to. The gap it closes: a 👎 on a FRESH investigation weighs nothing in recall trust (there is no catalog entry yet to decay), so without this pass the one human signal that says "this diagnosis is contested" never reaches the person about to make it permanent knowledge — the KB PR reviewer. Ledger-driven and idempotent like the other passes: votes are recomputed from the ledger every run, and a hidden per-trigger marker in the posted comment is the "already surfaced" record — no mutable store. It only ever comments; labels, closes and merges stay with the other passes and with humans.

func (Contested) Run added in v0.8.0

func (p Contested) Run(ctx context.Context) error

Run posts one warning comment per (open KB PR, contested trigger). Per-item failures are logged and skipped so one flaky PR never starves the rest.

type ContestedForge added in v0.8.0

type ContestedForge interface {
	Comment(ctx context.Context, number int, body string) error
	ListIssueCommentBodies(ctx context.Context, number int) ([]string, error)
	IsPROpen(ctx context.Context, number int) (bool, error)
}

ContestedForge is the forge surface the Contested pass needs. Kept separate from Forge: comment listing and PR-state lookup exist for this pass alone, and widening the shared interface would force every other pass's test fake to grow two irrelevant methods.

type ContestedSource added in v0.8.0

type ContestedSource interface {
	ContestedTriggers() []outcome.ContestedTrigger
}

ContestedSource yields the triggers whose delivered conclusion has standing 👎 votes and a KB link (the outcome ledger's ContestedTriggers view).

type Dedup

type Dedup struct {
	Forge     Forge
	Threshold float64 // title-Jaccard fallback threshold for markerless PRs; default 0.6 when 0
	Log       *slog.Logger
}

Dedup collapses near-identical open KB PRs: the lowest-numbered PR in a cluster is canonical; the rest are closed with a back-ref comment. Conservative by design (high similarity threshold) — a missed merge is cheaper than a wrong close.

func (Dedup) Run

func (d Dedup) Run(ctx context.Context) error

Run clusters open PRs and closes duplicates — fingerprint-first (deterministic DupFingerprint marker), falling back to title-Jaccard for markerless PRs.

type Forge

type Forge interface {
	ListPRsByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)
	ListIssuesByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)
	Comment(ctx context.Context, number int, body string) error
	ReplaceLabel(ctx context.Context, number int, remove, add string) error
	Close(ctx context.Context, number int) error
	OpenIssue(ctx context.Context, inv providers.Investigation) (providers.Ref, error)
}

Forge is the forge surface the groomer needs (all read/label/close — never merge).

type Guard added in v0.11.0

type Guard struct {
	Inner  GuardedForge
	DryRun bool
	Audit  audit.Auditor // nil-safe: nil drops records (actions are still slog-logged)
	Log    *slog.Logger
}

Guard is the sweep-safety seam around the forge: reads pass through untouched; every write is recorded in the audit chain and, in dry-run, logged instead of executed. One wrapper gives every pass dry-run + audit without touching the passes themselves — the KB mirror of action.NewAuditedExecutor.

func (Guard) Close added in v0.11.0

func (g Guard) Close(ctx context.Context, number int) error

Close closes a PR (audited; skipped in dry-run).

func (Guard) Comment added in v0.11.0

func (g Guard) Comment(ctx context.Context, number int, body string) error

Comment posts a PR comment (audited; skipped in dry-run).

func (Guard) IsPROpen added in v0.11.0

func (g Guard) IsPROpen(ctx context.Context, number int) (bool, error)

IsPROpen passes through to the wrapped forge.

func (Guard) ListClosedUnmergedPRsByLabel added in v0.11.0

func (g Guard) ListClosedUnmergedPRsByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)

ListClosedUnmergedPRsByLabel passes through to the wrapped forge.

func (Guard) ListIssueCommentBodies added in v0.11.0

func (g Guard) ListIssueCommentBodies(ctx context.Context, number int) ([]string, error)

ListIssueCommentBodies passes through to the wrapped forge.

func (Guard) ListIssuesByLabel added in v0.11.0

func (g Guard) ListIssuesByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)

ListIssuesByLabel passes through to the wrapped forge.

func (Guard) ListPRsByLabel added in v0.11.0

func (g Guard) ListPRsByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)

ListPRsByLabel passes through to the wrapped forge.

func (Guard) OpenIssue added in v0.11.0

func (g Guard) OpenIssue(ctx context.Context, inv providers.Investigation) (providers.Ref, error)

OpenIssue opens a knowledge-gap issue (audited; skipped in dry-run).

func (Guard) OpenRetirePR added in v0.11.0

func (g Guard) OpenRetirePR(ctx context.Context, entryPath, body string) (providers.Ref, error)

OpenRetirePR opens a retire PR for a decayed entry (audited; skipped in dry-run).

func (Guard) ReplaceLabel added in v0.11.0

func (g Guard) ReplaceLabel(ctx context.Context, number int, remove, add string) error

ReplaceLabel swaps a label on a PR (audited; skipped in dry-run).

type GuardedForge added in v0.11.0

type GuardedForge interface {
	Forge
	RetireForge
	ContestedForge
}

GuardedForge is the union of every read and write the grooming passes perform — the surface Guard wraps. Composed from the pass role interfaces so it stays in sync with them by construction (RetireForge adds ListClosedUnmergedPRsByLabel + OpenRetirePR; ContestedForge adds ListIssueCommentBodies + IsPROpen; overlapping methods across the embedded sets are legal since Go 1.14). *github.Client satisfies it (pinned in internal/app).

type LedgerResolutionChecker

type LedgerResolutionChecker struct {
	Ledger interface {
		Episodes() ([]outcome.Episode, error)
	}
}

LedgerResolutionChecker reports a curated PR's incident has resolved when the outcome ledger holds a matching resolved episode. The join is keyed on the deterministic dedup fingerprint (resource+cause) carried in the PR body and stamped on every ledger open — stable across the LLM's prose, so a reworded re-investigation of one incident still resolves the matching PR. A PR filed before the fingerprint was wired carries no marker; it falls back to a whitespace-normalized title join. Source-agnostic: it reads the ledger's episodes, never a trigger-specific API.

func (LedgerResolutionChecker) IsResolved

IsResolved implements ResolutionChecker.

type Lifecycle

type Lifecycle struct {
	Forge      Forge
	StaleAfter time.Duration    // 0 disables the sweep
	Now        func() time.Time // injectable clock; nil ⇒ time.Now
	Log        *slog.Logger
}

Lifecycle closes stale, unprotected KB artifacts — those with no forge activity within StaleAfter. A PR whose age is unknown (zero UpdatedAt) is never closed.

func (Lifecycle) Run

func (l Lifecycle) Run(ctx context.Context) error

Run closes stale, unprotected artifacts with a comment.

type Pass

type Pass interface {
	Run(ctx context.Context) error
}

Pass is one grooming pass (dedup, queue, lifecycle…). Each is independent and resilient: a pass error is logged, not fatal — the agent runs the rest.

type Queue

type Queue struct {
	Forge   Forge
	Checker ResolutionChecker
	Log     *slog.Logger
}

Queue applies the merge condition: a quality-passing (solved) PR becomes ready-to-merge when the incident is resolved, OR when a human has labelled it accepted. Unresolved + unaccepted PRs wait (surfaced, never auto-queued).

func (Queue) Run

func (q Queue) Run(ctx context.Context) error

Run gates each solved PR onto the decision-ready queue.

type Recurrence

type Recurrence struct {
	Forge  Forge
	Ledger interface {
		Episodes() ([]outcome.Episode, error)
	}
	Threshold int // default 3 when 0
	// Suppressed, when set, is the set of KB entries a human closed WITHOUT merging
	// (keyed by DupFingerprint). Their episodes are counted SEPARATELY — silently,
	// keyed on the fingerprint — and, past the threshold, escalate via a knowledge-gap
	// issue that LINKS the closed PR ("closed unmerged but recurred N times —
	// reconsider?") instead of the plain gap message. A close-without-merge is a
	// deliberate "no": we respect it and never reopen the PR. Nil disables the branch
	// (behaviour is then identical to the pre-suppression pass).
	Suppressed SuppressionSource
	Log        *slog.Logger
}

Recurrence opens ONE knowledge-gap issue when an unresolved incident pattern recurs at least Threshold times. It is ledger-driven and idempotent: it recomputes counts from the outcome ledger every run and opens an issue only when no knowledge-gap issue for that pattern already exists — so re-running never double-opens (no mutable store or watermark).

func (Recurrence) Run

func (r Recurrence) Run(ctx context.Context) error

Run opens a knowledge-gap issue for each pattern that crosses the threshold and has none open yet. Episodes belonging to a suppressed (closed-unmerged) entry are routed to a separate escalation that links the closed PR rather than the generic gap issue.

type ResolutionChecker

type ResolutionChecker interface {
	IsResolved(ctx context.Context, pr providers.CuratedIssue) (bool, error)
}

ResolutionChecker reports whether the incident behind a curated PR has cleared (alert resolved / resource healthy). Implemented read-only over the cluster.

type RetireForge added in v0.10.0

type RetireForge interface {
	ListPRsByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)
	ListClosedUnmergedPRsByLabel(ctx context.Context, label string) ([]providers.CuratedIssue, error)
	OpenRetirePR(ctx context.Context, entryPath, body string) (providers.Ref, error)
}

RetireForge is the forge surface the Retirement pass needs (consumer-side, like ContestedForge — widening the shared Forge would bloat every other pass's fake).

type RetireStats added in v0.10.0

type RetireStats interface {
	OpenCounts() (map[string]outcome.Aggregate, error)
}

RetireStats is the ledger view the pass needs: the per-entry outcome roll-up.

type Retirement added in v0.10.0

type Retirement struct {
	Forge           RetireForge
	Stats           RetireStats
	MinObservations int     // sustained-decay bar: total observations before retirement is considered
	Floor           float64 // retire when Factor(Prior) < Floor
	Prior           float64 // k — must equal recall's outcome_prior so both gates agree
	Log             *slog.Logger
}

Retirement opens a human-reviewed "retire" PR for a merged catalog entry whose outcome track record has decayed below the trust floor and stayed there across at least MinObservations observations — closing the garbage-collection half of the learning loop. It never merges and never deletes: a human is the load-bearing gate, and the PR only stamps `status: retired` frontmatter (the entry stays in git history). Ledger-driven and idempotent like the other passes — a hidden per-entry marker in the PR body is the "already proposed" record, and a closed-unmerged retire PR carrying the marker is a human veto that is never re-nagged. Opt-in (see config): retirement is a judgment call an operator enables.

func (Retirement) Run added in v0.10.0

func (p Retirement) Run(ctx context.Context) error

Run proposes one retire PR per sustained-decay candidate. Per-item forge failures are logged and skipped so one flaky entry never starves the rest.

type Suppress added in v0.11.0

type Suppress struct {
	Forge  Forge
	Source SuppressionSource
	Log    *slog.Logger
}

Suppress closes open KB PRs that RE-DRAFT an entry a human already rejected (closed without merging). The file-time drafter's dedup checks only OPEN PRs and MERGED entries, so a recurring permanently-benign incident re-opens a fresh PR on every recurrence — the core of PR fatigue. Suppress honors the human "no": it closes the re-draft with a back-reference to the original close and its reason, and leaves the "reconsider" path to Recurrence's threshold escalation (which links, never reopens). Protected (human-touched) PRs are never closed, and the comment-first / don't-close-on-comment-failure ordering mirrors Lifecycle.

func (Suppress) Run added in v0.11.0

func (s Suppress) Run(ctx context.Context) error

Run closes every unprotected open KB PR whose DupFingerprint a human previously rejected. Per-item forge failures are logged and skipped.

type SuppressedEntry added in v0.4.0

type SuppressedEntry struct {
	Fingerprint string // curator DupFingerprint (resource+cause); the join key against ledger episodes
	PRNumber    int    // the closed-unmerged KB PR to reference in the escalation
	Reason      string // the close-reason label, when one distinguishes it (e.g. "wontfix"); "" otherwise
}

SuppressedEntry is a KB entry a human closed WITHOUT merging — a deliberate "not knowledge-base-worthy". It is keyed on the entry's DupFingerprint so the recurrence pass can keep counting the incident silently and, past the threshold, escalate via a knowledge-gap issue that LINKS the closed PR — never reopening it.

type SuppressionSource added in v0.4.0

type SuppressionSource interface {
	Suppressed(ctx context.Context) (map[string]SuppressedEntry, error)
}

SuppressionSource yields the set of suppressed fingerprints (closed-unmerged KB entries a human rejected), keyed by DupFingerprint. Recurrence consults it to escalate-via-issue instead of re-litigating a human "no".

type Sweeper added in v0.11.0

type Sweeper struct {
	Agent    Agent
	Interval time.Duration // <= 0 ⇒ DefaultSweepInterval
	Log      *slog.Logger
}

Sweeper runs the grooming Agent on a fixed interval until ctx is done — the in-server counterpart of the `lore curate` CronJob. The first sweep happens one FULL interval after start, never immediately: it is launched on every leadership (re-)acquisition, and a flapping leader must not stampede the forge with back-to-back listing storms. All passes are idempotent, so an occasional double-run across a flap is safe, just wasteful.

func (Sweeper) Run added in v0.11.0

func (s Sweeper) Run(ctx context.Context)

Run sweeps every Interval until ctx is cancelled.

Jump to

Keyboard shortcuts

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