evocoupling

package
v0.1.2 Latest Latest
Warning

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

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

Documentation

Overview

Package evocoupling computes evolutionary coupling between packages via co-change kernel correlation.

Index

Constants

View Source
const (
	ReasonOK                  = "ok"
	ReasonInsufficientCommits = "insufficient_commits"
	ReasonSparseWindow        = "sparse_window"
	ReasonDensityFallback     = "density_fallback"
)

Reason codes for AutoSigmaResult.Reason. Stable strings — they appear in JSON output and downstream consumers may branch on them.

Variables

This section is empty.

Functions

func CoOccurrences

func CoOccurrences(seriesA, seriesB []bool) int

CoOccurrences returns n_11 — the number of bins where both sequences are true. Surfaced for the min-support filter in Analyze. Returns 0 when inputs are unequal-length (no signal is defensible).

func EffectiveSampleSize

func EffectiveSampleSize(
	events []time.Time,
	centerAt time.Time,
	sigma time.Duration,
) float64

EffectiveSampleSize returns the Gaussian-kernel effective sample size for the given event timestamps, kernel bandwidth, and centering time.

ESS = (Σ wᵢ)² / Σ wᵢ², where wᵢ = exp(-Δᵢ² / 2σ²) and Δᵢ = centerAt - eventᵢ.

Returns 0 when events is empty, sigma is non-positive, or no event lies within the kernel's effective support. Documented choice: ESS at σ=0 is the limit of a degenerate point-mass kernel and not meaningful for the downstream binary search; returning 0 keeps the search bounds well-defined.

Implementation note: events more than essTailCutoffSigmas × σ from centerAt are skipped as an O(N) optimization. The Gaussian falls off so fast that retaining them changes ESS by less than the 5% tolerance the auto-picker converges to.

func Entropy

func Entropy(series []bool) float64

Entropy returns Shannon entropy H(A) of a binary sequence in bits. Returns 0 for empty input or degenerate distributions (all-true or all-false), matching the convention that 0·log0 := 0.

func Gaussian

func Gaussian(delta, sigma time.Duration) float64

Gaussian kernel: exp(-(delta^2) / (2 * sigma^2)).

func MutualInformation

func MutualInformation(seriesA, seriesB []bool) float64

MutualInformation returns the plug-in Shannon mutual information between two equal-length binary sequences, in bits.

No bias correction is applied — this is the raw estimator. Use NMI for user-facing output; MutualInformation is exported for the conditional-MI v2 work and for tests that need the uncorrected baseline.

Returns 0 when len(a) == 0, when sequences differ in length (no signal is defensible from mismatched inputs), or when either marginal is degenerate (one outcome with probability 1).

func NMI

func NMI(seriesA, seriesB []bool) float64

NMI returns Miller-Madow-corrected normalized mutual information between two binary presence sequences. Result is in [0, 1], clipped to 0 when the bias correction exceeds the plug-in estimate.

Normalization is by min(H(A), H(B)) per the design doc — bounded [0, 1] and information-symmetric (perfect correlation and perfect anti- correlation both yield 1).

Returns NaN when:

  • inputs are unequal-length or shorter than miMinBins (the contingency is too sparse to learn from);
  • either marginal is degenerate (min(H(A), H(B)) == 0), so the ratio is undefined.

The NaN return is a deliberate signal to the caller — a degenerate or underpowered pair carries no information about coupling. Downstream callers test with math.IsNaN and surface low_support.

func PearsonBinned

func PearsonBinned(seriesA, seriesB []bool) float64

PearsonBinned returns the phi coefficient (Pearson correlation on two binary sequences). Result is in [-1, 1].

Returns NaN when inputs are unequal-length, shorter than miMinBins, or when either sequence is degenerate (variance = 0). NaN propagates the same low-support signal as NMI for caller-side handling.

Types

type AdviseOptions

type AdviseOptions struct {
	// MinCorrelation drops pairs below the threshold. Zero keeps every
	// pair the coupling analysis surfaced.
	MinCorrelation float64

	// IncludeLowSupport keeps pairs flagged LowSupport. Off by default:
	// young or sparse histories mark almost every pair low-support, and
	// advisories built on them are noise.
	IncludeLowSupport bool
}

AdviseOptions tunes the advisory pass.

type Advisory

type Advisory struct {
	Touched     string  `json:"touched"`
	Expected    string  `json:"expected"`
	Correlation float64 `json:"correlation"`
}

Advisory flags a co-change expectation a change set did not meet: the touched package historically changes together with the expected one, and this time it did not. Purely informational — the review command renders advisories without affecting its exit code.

func Advise

func Advise(pairs []CouplingPair, touched map[string]bool, opts AdviseOptions) []Advisory

Advise compares a co-change model (pairs from Analyze) against the set of packages a change touched. For each qualifying pair with exactly one side touched, it emits an advisory for the untouched side. Output is sorted by correlation (descending), then by touched/expected name.

type AutoSigmaOptions

type AutoSigmaOptions struct {
	// TargetStddev is the desired correlation-estimator standard deviation.
	// Default 0.05 ("two decimal places of stability"). Drives required
	// ESS via required = 1 / TargetStddev². Smaller stddev → larger sigma.
	TargetStddev float64

	// CoveragePercentile selects which point on the ESS distribution
	// across the evaluation grid must meet required ESS. 50 = median
	// ("typical density"), 25 = stricter coverage ("honest in sparse
	// regions"). Range [1, 99]; default 50.
	CoveragePercentile float64

	// EvalPoints is the number of evaluation centerAt timestamps spread
	// uniformly across [WindowStart, WindowEnd]. Default 20; min 3 so the
	// percentile has at least a start/middle/end to interpolate.
	EvalPoints int

	// WindowStart and WindowEnd bound the analysis window. The binary
	// search picks σ in [1 minute, WindowEnd - WindowStart].
	WindowStart time.Time
	WindowEnd   time.Time
}

AutoSigmaOptions configures DeriveSigma. Zero values are replaced with defaults — passing AutoSigmaOptions{WindowStart: ..., WindowEnd: ...} is the common case.

type AutoSigmaResult

type AutoSigmaResult struct {
	Sigma             time.Duration
	RequiredESS       float64
	AchievedESSMedian float64
	AchievedESSP25    float64
	LowConfidence     bool
	Reason            string
	DensityCap        time.Duration
}

AutoSigmaResult is the picker's output. Sigma is always populated (the fallback path returns window/3); LowConfidence flags whether the chosen sigma actually achieves the target precision. DensityCap is the session-resolution ceiling applied to the sigma search (0 = uncapped — the cadence has no session structure).

func DeriveSigma

func DeriveSigma(events []time.Time, opts AutoSigmaOptions) AutoSigmaResult

DeriveSigma picks the smallest Gaussian-kernel bandwidth that delivers the user's target correlation precision across the analysis window.

Algorithm (per the sigma auto-picker design doc, density-aware revision):

  1. required_ess := 1 / target_stddev²
  2. Build EvalPoints grid at quantiles of the event times — precision is evaluated where estimates are made, not where the calendar is empty.
  3. Cap the search's upper bound at the session-resolution ceiling (densityCap): a σ wider than a work session blends distinct sessions into one blob, which on burst-cadence repos turns every pair into a precise estimate of a meaningless quantity.
  4. Binary search σ in [1 min, min(window, cap)] for required ESS; converge to within 5% relative tolerance.
  5. If required ESS is unreachable at the upper bound, retry with the ESS floor (30 ≈ stddev 0.18). If even that is unreachable but session structure exists, keep σ at the session ceiling — precision is lost either way, so resolution wins over a precise estimate of a smeared quantity. Both return reason density_fallback. Only cadence with no session structure falls back to σ = window/3 with insufficient_commits. All fallbacks mark LowConfidence.

type CouplingPair

type CouplingPair struct {
	PackageA      string   `json:"package_a"`
	PackageB      string   `json:"package_b"`
	Correlation   float64  `json:"correlation"`
	PearsonBinned *float64 `json:"pearson_binned,omitempty"`
	NMI           *float64 `json:"nmi,omitempty"`
	Divergence    *float64 `json:"divergence,omitempty"`
	LowSupport    bool     `json:"low_support,omitempty"`
}

CouplingPair represents the evolutionary coupling between two packages.

Correlation is the kernel-weighted Pearson on continuous event streams — the v1 metric, unchanged. The pointer-valued PearsonBinned, NMI, and Divergence fields are populated only when Options.MIEnabled is true, and are NaN-suppressed: a NaN result (degenerate marginal, < miMinBins bins) leaves the pointer nil so the JSON output omits the field rather than emitting a non-marshallable NaN. LowSupport flags n_11 below the configured minimum.

func Analyze

func Analyze(commits []TimedPackageSet, opts Options) []CouplingPair

Analyze computes evolutionary coupling between packages from commit history.

The kernel-weighted Pearson pass always runs. When opts.MIEnabled is set, an independent binned-MI pass runs in parallel (sigma drives both the kernel bandwidth and the bin width — apples-to-apples by design). The two passes are merged by package-pair key: a pair surfaced by either pass carries both metric families.

type KernelFunc

type KernelFunc func(delta, sigma time.Duration) float64

KernelFunc computes the weight for a time delta given a bandwidth (sigma). Both are durations. Returns a value in [0, 1] where 0 = no influence, 1 = maximum influence (delta = 0).

type Options

type Options struct {
	Sigma   time.Duration
	Kernel  KernelFunc
	MinCorr float64

	// MIEnabled toggles the binned mutual-information metrics. When false
	// (v1 default), Analyze emits only the existing Correlation column
	// and JSON output is byte-stable with pre-MI consumers.
	MIEnabled bool

	// MIMinSupport is the n_11 floor below which a pair is flagged
	// LowSupport. Zero is replaced with defaultMIMinSupport (3) so
	// `Options{MIEnabled: true}` is a sensible call.
	MIMinSupport int

	// WindowStart anchors the presence matrix; commits before it are
	// dropped from the MI computation. Zero value falls back to the
	// earliest commit time in the input — common-case ergonomics.
	WindowStart time.Time
}

Options configures evolutionary coupling analysis.

type PackageResolver

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

PackageResolver maps file paths to package names via a prefix trie.

func NewPackageResolver

func NewPackageResolver(boundaries []analyzer.PackageBoundary) *PackageResolver

NewPackageResolver builds a resolver from analyzer boundaries.

func (*PackageResolver) Resolve

func (r *PackageResolver) Resolve(filePath string) []string

Resolve returns the package names for a file path. Returns the deepest matching prefix. Returns nil if no match.

func (*PackageResolver) ResolveCommit

func (r *PackageResolver) ResolveCommit(files []string) map[string]struct{}

ResolveCommit returns the set of packages touched by a commit's changed files.

type PresenceMatrix

type PresenceMatrix struct {
	// Bins lists bin start times in chronological order, length n_bins.
	Bins []time.Time
	// Packages lists package names in lexicographic order, length n_packages.
	Packages []string
	// Present is indexed [binIdx][pkgIdx]; true iff Packages[pkgIdx]
	// appeared in any commit during the bin [Bins[binIdx], Bins[binIdx]+binWidth).
	Present [][]bool
}

PresenceMatrix is a [n_bins][n_packages] binary matrix indicating which packages had at least one commit in each fixed-width time bin.

The matrix is binary: multiple commits in a bin collapse to a single presence flag. Frequency information is the job of the kernel-weighted Pearson pipeline; this representation feeds the mutual-information estimator, which only cares whether the event happened.

Shape is preserved for forward-compatibility with conditional mutual information (v2 of the MI extension): Bins + Packages exposed as the stable axes, Present accessed by [binIdx][pkgIdx] so a third conditioning axis can be added without refactor.

func BuildPresenceMatrix

func BuildPresenceMatrix(
	commits []TimedPackageSet,
	windowStart time.Time,
	binWidth time.Duration,
) *PresenceMatrix

BuildPresenceMatrix bins commits by binWidth into a PresenceMatrix.

windowStart anchors the first bin; subsequent bins start at windowStart + k·binWidth. A commit whose Time falls in [windowStart + k·binWidth, windowStart + (k+1)·binWidth) marks every package it touches as present in bin k. Multiple commits in the same bin collapse to a single presence flag.

The number of bins is derived from the commit time span: ceil((maxTime - windowStart + 1) / binWidth) bins, capacity for the latest commit inclusive. If commits is empty or binWidth is non-positive, returns a matrix with no bins and no packages.

Packages absent from every commit are still listed in Packages with all- false rows iff they appear in any commit; packages never touched are not represented at all. Edge commits at windowStart land in bin 0; commits before windowStart are dropped.

type TimedPackageSet

type TimedPackageSet struct {
	Time     time.Time
	Packages map[string]struct{}
}

TimedPackageSet represents the packages touched by a single commit.

Jump to

Keyboard shortcuts

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