linkage

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package linkage is a pure, in-memory commit↔run matcher. It reads AgentRun projections and synthetic commit fixtures only — no database, no git, no subprocesses. It does not persist the edges it produces.

Index

Constants

View Source
const (
	ReasonBothTimestampsMissing  = "both_timestamps_missing"
	ReasonOutsideTimeframe       = "outside_timeframe"
	ReasonInvalidCommitTimestamp = "invalid_commit_timestamp"
	ReasonAgentMismatch          = "agent_mismatch"
	ReasonUserEmailMismatch      = "user_email_mismatch"
	ReasonBranchMismatch         = "branch_mismatch"

	ReasonNoFileOverlap   = "no_file_overlap"
	ReasonNoStepFinalized = "no_step_finalized_match"
	ReasonNoCandidates    = "no_candidates"
	ReasonMergeCommit     = "merge_commit"

	// A malformed trailer falls through to the heuristic steps but remains
	// visible as a diagnostic.
	ReasonEntireCheckpointTrailerInvalid = "entire_checkpoint_trailer_invalid"
	// The trailer named a checkpoint we do not hold the sessions for yet.
	ReasonAwaitingCheckpoint = "awaiting_checkpoint"
	// The commit is signed by an agent and the repository holds no session at
	// all: nobody has sent us the run that wrote it. Written by the resolver,
	// not here — an empty candidate set does not establish it. See
	// CandidatePrefilterSound.
	ReasonAwaitingAgentSession = "awaiting_agent_session"
	// The commit was eligible when queued but disappeared before resolution,
	// usually because a retained PR ref was pruned or force-updated.
	ReasonGitObjectUnavailable = "git_object_unavailable"
	// Somebody removed the last session linked to the commit by hand.
	ReasonManuallyUnlinked = "manually_unlinked"
)

Reason codes for exclusion / unmatched diagnostics. The first group is reported by the filter step that emptied the candidate set (see FilteredWithReason), so an unmatched edge names the step that ruled every run out instead of the generic ReasonNoCandidates.

View Source
const (
	// An agent wrote the commit and we do not hold the session that did.
	// Nothing is wrong with the matching; the record has not reached us.
	ClassAwaitingSession = "awaiting_session"
	// Nothing about the commit says an agent wrote it, or something does and no
	// session we hold accounts for it. Either way the answer is not more data.
	ClassNoEvidence = "no_evidence"
	// Not an unattributed commit at all: it matched, it is a merge, or it has
	// no verdict yet.
	ClassNotApplicable = "not_applicable"
)

Outcome classes group the reasons above into what a reader can act on. The set is closed and OutcomeClass is the only place a reason is mapped to one, so a new reason is classified once rather than in every report that shows it.

View Source
const ReviewConfidence = 0.7

ReviewConfidence is the confidence at which a match stops needing a person to look at it. The resolver will not add a weaker edge to a commit that already has one this strong, and the session list reads the same value, so a commit and the sessions on it cannot disagree about what counts as settled.

Variables

View Source
var DefaultMatchSteps = []string{
	"commit_ref",
	"timeframe",
	"coauthor_agent",
	"user_email",
	"start_commit",
	"branch",
	"files_overlap",
}

DefaultMatchSteps is the step order run when Config.EnabledMatchSteps is unset. commit_ref runs first: it links a run that recorded creating this exact SHA, evidence no soft filter can improve on, so it must not be gated behind one that can drop the run first. files_overlap always runs last: it is the only ranking finalizer, everything before it is a filter or a hard link.

Functions

func AwaitingSessionReasons

func AwaitingSessionReasons() []string

AwaitingSessionReasons is the list a query needs when it has to make the same split in SQL. It is passed in as a parameter rather than written out there, so the set stays defined here alone.

func DefaultAgentAliases

func DefaultAgentAliases() map[string]string

DefaultAgentAliases maps a Co-Authored-By agent name to the vendor slug it identifies. Keys are matched case-insensitively by callers; store them lower-cased.

The keys must be the vocabulary commit enrichment actually emits: gitrepository identifies agents by trailer *email*, then reports a fixed slug ("claude", "codex", "cursor", "copilot", ...), never a display name. Aliases for display names ("claude code", "anthropic", "openai") could therefore never be hit. Enrichment slugs no agent of ours can claim (aider, devin, jules, ...) are deliberately absent: no candidate can claim them, so coauthor_agent rules every candidate out, which is correct.

func NormalizeAgent

func NormalizeAgent(raw string, aliases map[string]string) string

NormalizeAgent maps an agent string to its configured canonical slug, falling back to a trimmed lowercase copy when it is unmapped.

func OutcomeClass

func OutcomeClass(reason string) string

OutcomeClass groups an edge's reason. An empty reason is not applicable: a matched edge carries none.

func TrailerAttribution

func TrailerAttribution(commit EnrichedCommit, cfg Config) []string

TrailerAttribution returns the commit's normalized, order-stable agent slugs.

Types

type Commit

type Commit struct {
	OID         string `json:"oid"`
	AuthorEmail string `json:"author_email"`
	CommittedAt string `json:"committed_at"`
}

Commit is what the matcher reads about one commit. Timestamps are RFC 3339 strings as git prints them with %cI; a value that does not parse counts as unknown rather than failing the match.

type CommitRef

type CommitRef struct {
	SHA  string
	Role Role
}

CommitRef is one role-tagged commit SHA carried on a RunCandidate.

type Config

type Config struct {
	ExcludeMergeCommits bool

	// Timing controls admission and the confidence discounts for late commits
	// and long-running sessions.
	Timing TimingConfig

	// Soft files_overlap finalizer gates. Hard-link steps (commit_ref,
	// start_commit) finalize earlier and ignore these. The score they gate is
	// weighted containment — edits count fully and reads count partially — see
	// steps.FilesOverlapStep.
	FilesOverlapMinScore        float64
	FilesOverlapMinIntersection int
	// The fallback admits lower containment only with more exact edits.
	FilesOverlapFallbackScore float64
	FilesOverlapFallbackEdits int
	// FilesOverlapMinScoreMargin defines the band in which a lower-scored
	// runner-up is recorded on the winning edge.
	FilesOverlapMinScoreMargin float64

	// FilesOverlapMinConfidence and FilesOverlapMaxConfidence clamp a
	// files_overlap match's confidence: even a perfect score is a soft
	// signal next to a hard link.
	FilesOverlapMinConfidence float64
	FilesOverlapMaxConfidence float64

	// FilesOverlapMaxLinksPerCommit bounds how many sessions one commit may be
	// credited to by this step. Zero or less is unbounded. Exact ties are
	// common on small commits and the score cannot separate them, so without a
	// ceiling one commit accretes every session that clears the floor.
	FilesOverlapMaxLinksPerCommit int

	// CommitRefConfidence is the confidence assigned to a commit_ref hard
	// link.
	CommitRefConfidence float64
	// TrailerConfidence is the confidence assigned to trailer attribution.
	TrailerConfidence float64
	// StartCommitConfidence is the confidence assigned to a start_commit
	// hard link.
	StartCommitConfidence float64

	AgentAliases      map[string]string
	EnabledMatchSteps []string
}

Config holds every matcher tunable. It owns the defaults and validation; callers only decode the external representation into it.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns the matcher's built-in tunables for standalone or test use. Callers needing overrides should copy this and mutate the copy; Config carries no globals.

func (Config) CandidatePrefilterSound

func (c Config) CandidatePrefilterSound() bool

CandidatePrefilterSound reports whether a candidate set restricted to commit_ref ∪ timeframe — the sessions whose window covers the commit, plus the sessions that recorded creating its SHA — is a superset of every edge this step order can finalize. The resolver consults it per resolution and loads the repository's candidates unfiltered when it is false: an ablation is a legitimate experiment, and one that silently loses matches the configured chain would have made is worse than a slow one.

It holds when every enabled step that can finalize an edge, other than commit_ref, runs after timeframe. Each clause:

  • After timeframe, a step only ever sees candidates timeframe kept, and the prefilter admits all of those, so its input is exactly the input a full scan would hand it.
  • commit_ref is exempt at any position. It finalizes only on a candidate that recorded creating this SHA, and the prefilter's ref arm loads every one of those whether or not its window covers the commit — so the set it picks its lowest RunID from is complete either way.
  • coauthor_agent, user_email and branch are exempt at any position: they never finalize, and they drop candidates one at a time on that candidate's own evidence, so a smaller input gives a correspondingly smaller output and admits nothing new.
  • Any other id — a custom or future step — counts as finalizing, and before timeframe it makes this false. A step this package cannot reason about must cost a full candidate load, not a silent loss.

timeframe itself need not be enabled when nothing but commit_ref and those filters is: such a chain can only finalize on the ref arm, which the prefilter always carries.

What the prefilter still moves when this holds is the diagnostic on an unmatched edge, never the edge: reason and candidates_remaining are read off a set timeframe's rejects were never loaded into.

func (Config) TimeframeTolerance

func (c Config) TimeframeTolerance() time.Duration

TimeframeTolerance is how far past a session's effective end a commit may still be matched to it. The timeframe step, the SQL candidate prefilter and the requeue arms all read it here, because a second spelling of the conversion is a second value they could disagree on.

func (Config) Validate

func (c Config) Validate() error

Validate rejects timing policy that would invert a discount or shrink the candidate window backwards. A non-positive full-confidence window remains the documented way to disable the long-window discount.

type Edge

type Edge struct {
	CommitSHA            string
	RunID                string // "" if unmatched or excluded
	AdditionalRepository bool
	Status               EdgeStatus
	Method               Method // "" if no attribution method applies
	Confidence           float64
	MatchedBy            string // matching path that finalized the edge
	TrailerAgents        []string
	Score                float64
	ScoreValid           bool // Score is only meaningful when true (files_overlap)
	CandidatesRemaining  int
	Reason               string // exclusion / unmatched reason; "" if matched
	Tie                  *TieDiagnostic
	TimeDiscount         *TimeDiscount
}

Edge is the matcher's in-memory output for one commit. This package does not persist it; a linkage_edges row is written by the resolver.

func MatchCommit

func MatchCommit(commit EnrichedCommit, candidates []RunCandidate, cfg Config, availableSteps []Step) ([]Edge, error)

MatchCommit resolves one commit's edges against the full candidate set. Merge exclusion runs first; then cfg.EnabledMatchSteps run in order, selected by id from availableSteps, until one returns final edges or the chain runs out of steps. A finalizer may return several matched edges; an excluded or unmatched result always contains exactly one. This function never touches Postgres or git and does not persist the edges it returns.

func MatchCommits

func MatchCommits(commits []EnrichedCommit, candidates []RunCandidate, cfg Config, availableSteps []Step) ([]Edge, error)

MatchCommits resolves edges for every commit against the same candidate set, preserving commit order while flattening multi-edge matches.

func MatchedEdge

func MatchedEdge(commit EnrichedCommit, candidate RunCandidate, method Method, confidence float64, candidatesRemaining int) Edge

MatchedEdge builds a status=matched Edge for a hard-link or ranked pick.

func TrailerEdge

func TrailerEdge(unmatched Edge, agents []string, cfg Config) Edge

TrailerEdge attributes an unmatched outcome to its co-author trailers.

type EdgeStatus

type EdgeStatus string

EdgeStatus is the outcome of matching one commit.

const (
	StatusMatched       EdgeStatus = "matched"
	StatusExcludedMerge EdgeStatus = "excluded_merge"
	StatusUnmatched     EdgeStatus = "unmatched"
)

type EnrichedCommit

type EnrichedCommit struct {
	Commit
	IsMerge          bool     `json:"is_merge"`
	AncestorOID      string   `json:"ancestor_oid,omitempty"`
	CoauthoredAgents []string `json:"coauthored_agents"`
	FilesTouched     []string `json:"files_touched"`
	BranchNames      []string `json:"branch_names"`
}

EnrichedCommit is a Commit with the derived facts the steps match on. AncestorOID is the commit the branch started from (the merge base with the base branch), FilesTouched its repository-relative paths, CoauthoredAgents the agent slugs named by Co-Authored-By trailers.

type Method

type Method string

Method identifies which step or attribution source finalized an edge.

const (
	MethodCommitRef        Method = "commit_ref"
	MethodStartCommit      Method = "start_commit"
	MethodFilesOverlap     Method = "files_overlap"
	MethodEntireCheckpoint Method = "entire_checkpoint"

	// MethodManual is a person's answer rather than a step's. It outranks every
	// heuristic method and survives re-resolution.
	MethodManual  Method = "manual"
	MethodTrailer Method = "trailer"

	// Reserved: not yet implemented.
	MethodCollector     Method = "collector"
	MethodBotConvention Method = "bot_convention"
)

type Role

type Role string

Role is the relationship between a session and a commit SHA (kept as an independent string type here so this package stays free of any database import).

const (
	RoleCreated    Role = "created"
	RoleMentioned  Role = "mentioned"
	RoleCheckedOut Role = "checked_out"
	RoleStarting   Role = "starting"
)

type RunCandidate

type RunCandidate struct {
	// RunID is the tree's root session id.
	RunID string
	// AdditionalRepository identifies evidence scoped to the commit repository
	// through an active additional-repository scope.
	AdditionalRepository bool
	// Agent is the vendor that ran, e.g. "claude_code". This is what a
	// Co-Authored-By trailer names.
	Agent string
	// Source is how the tree reached us ("upload", "entire_checkpoint"). It
	// says nothing about which agent ran, which is why Agent exists.
	Source    string
	UserEmail string // "" if unknown
	Branches  []string
	StartedAt *time.Time
	EndedAt   *time.Time
	// IngestedAt is when the upload carrying this tree arrived. It stands in
	// for a missing EndedAt: an upload cannot hold activity later than itself,
	// so a session that never recorded an end is still bounded.
	IngestedAt *time.Time
	// CommitRefs are this tree's stored commit refs.
	CommitRefs []CommitRef
	// StartingCommitSHA is the role=starting ref's SHA, if any (denormalized
	// for the start_commit step; also present in CommitRefs).
	StartingCommitSHA string
	// FilesTouched is the union of the tree's read and edit evidence.
	FilesTouched []string
	// FilesEdited is the subset the tree edited.
	FilesEdited []string
}

RunCandidate is the in-memory projection of one stored session tree and the matcher evidence derived from it at ingest. Callers build it from stored ingest and evidence data; the matcher never touches the database.

func PickLowestRunID

func PickLowestRunID(candidates []RunCandidate) RunCandidate

PickLowestRunID returns the candidate with the lexicographically smallest RunID — the deterministic tie-break every hard-link and ranking step uses when more than one candidate remains. Panics on an empty slice: callers only reach for this after confirming at least one match.

func (RunCandidate) AgentOrSource

func (c RunCandidate) AgentOrSource() string

AgentOrSource is what the coauthor step compares against a commit's Co-Authored-By trailers: the vendor that ran, falling back to how the tree arrived when a candidate carries no vendor.

func (RunCandidate) EffectiveEndedAt

func (c RunCandidate) EffectiveEndedAt() *time.Time

EffectiveEndedAt is the end the matcher measures a commit against: the recorded end, or the upload that carried the tree when it recorded none.

type Step

type Step interface {
	// ID is the stable step id matched against Config.EnabledMatchSteps.
	ID() string
	Apply(commit EnrichedCommit, candidates []RunCandidate, cfg Config) StepResult
}

Step is one ordered filter/link step in the match pipeline.

type StepResult

type StepResult struct {
	Candidates []RunCandidate
	Edges      []Edge
	// Reason explains why this step dropped candidates. The pipeline only
	// reports it when the step emptied a non-empty set, which is the one case
	// where it is the whole story.
	Reason string
}

StepResult is the outcome of one match step: exactly one of Candidates or Edges is set. Use Filtered/FilteredWithReason/Final/FinalEdges to construct it.

func Filtered

func Filtered(candidates []RunCandidate) StepResult

Filtered returns a StepResult that narrows the candidate set without finalizing a match.

func FilteredWithReason

func FilteredWithReason(candidates []RunCandidate, reason string) StepResult

FilteredWithReason is Filtered plus the diagnostic a caller should see if this step is the one that left nothing to match against.

func Final

func Final(edge Edge) StepResult

Final returns a StepResult that finalizes edge and stops the step chain.

func FinalEdges

func FinalEdges(edges []Edge) StepResult

FinalEdges returns a StepResult that finalizes several edges at once.

func (StepResult) IsFinal

func (r StepResult) IsFinal() bool

IsFinal reports whether this result finalized one or more edges.

type TieDiagnostic

type TieDiagnostic struct {
	Resolution TieResolution
	Peers      []TiePeer
}

type TiePeer

type TiePeer struct {
	RunID string
	Score float64
}

type TieResolution

type TieResolution string
const (
	TieResolutionLinkedAll    TieResolution = "linked_all"
	TieResolutionLinkedCapped TieResolution = "linked_capped"
	TieResolutionScoreOrder   TieResolution = "score_order"
)

type TimeDiscount

type TimeDiscount struct {
	GapSeconds    float64
	WindowSeconds float64
	SyntheticEnd  bool
	Factor        float64
}

TimeDiscount is why a files_overlap edge's confidence was reduced for when the work happened rather than for what it touched: how far past the session's effective end the commit landed, how wide the session's window was, whether that end was the upload time, and the resulting factor.

type TimingConfig

type TimingConfig struct {
	// Tolerance widens a run's [started_at, ended_at] window on the end side.
	Tolerance time.Duration
	// GapConfidenceFloor is the share of its confidence a match keeps when the
	// commit sits a full tolerance past the session's effective end.
	GapConfidenceFloor float64
	// FullConfidenceWindow is the longest session window that keeps full
	// confidence. Zero or less disables the long-window discount.
	FullConfidenceWindow time.Duration
}

TimingConfig holds the matcher policy whose units are durations rather than the seconds used by the external TOML representation.

Directories

Path Synopsis
Package gitevidence recognises git activity in text an agent captured — a command it ran, the output it got back.
Package gitevidence recognises git activity in text an agent captured — a command it ran, the output it got back.
Package sessioncorrelate derives the evidence the commit matcher needs from a session tree: repo-relative file paths, role-tagged commit refs, and the branches and working directories the tree was seen in.
Package sessioncorrelate derives the evidence the commit matcher needs from a session tree: repo-relative file paths, role-tagged commit refs, and the branches and working directories the tree was seen in.
Package steps holds one file per match step, each a pure linkage.Step.
Package steps holds one file per match step, each a pure linkage.Step.

Jump to

Keyboard shortcuts

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