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
- Variables
- func AwaitingSessionReasons() []string
- func DefaultAgentAliases() map[string]string
- func NormalizeAgent(raw string, aliases map[string]string) string
- func OutcomeClass(reason string) string
- func TrailerAttribution(commit EnrichedCommit, cfg Config) []string
- type Commit
- type CommitRef
- type Config
- type Edge
- func MatchCommit(commit EnrichedCommit, candidates []RunCandidate, cfg Config, ...) ([]Edge, error)
- func MatchCommits(commits []EnrichedCommit, candidates []RunCandidate, cfg Config, ...) ([]Edge, error)
- func MatchedEdge(commit EnrichedCommit, candidate RunCandidate, method Method, ...) Edge
- func TrailerEdge(unmatched Edge, agents []string, cfg Config) Edge
- type EdgeStatus
- type EnrichedCommit
- type Method
- type Role
- type RunCandidate
- type Step
- type StepResult
- type TieDiagnostic
- type TiePeer
- type TieResolution
- type TimeDiscount
- type TimingConfig
Constants ¶
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" // 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.
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.
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 ¶
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 ¶
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 ¶
NormalizeAgent maps an agent string to its configured canonical slug, falling back to a trimmed lowercase copy when it is unmapped.
func OutcomeClass ¶
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 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 ¶
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 ¶
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.
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.
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"`
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).
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 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. |