snapshot

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package snapshot captures one complete doppel analysis run as plain data, so that two runs can be compared.

Nothing else in the pipeline has a notion of "a run": comparator compares two functions, analyzer compares two fingerprints, reporter renders one result. A Snapshot is the missing noun — everything an impact comparison needs about a corpus at a point in time, and deliberately nothing more. It is derived output, never an input to any score.

Three rules govern the schema, all load-bearing:

Determinism. The repo's invariant is that an unchanged tree produces byte-identical output, so every map is flattened into a sorted slice before it reaches JSON and every slice has a total order. encoding/json does sort map keys, so a map field would marshal deterministically too; sorted slices are used anyway so the ordering is visible in the type rather than an emergent property of the encoder, and so the Go code that reads a snapshot sees the same order the file does.

Identity over position. A diff is only meaningful if a function can be recognised across two runs, so units and pairs are keyed by name and never by index. Indices shift when any file changes, which is exactly the case a diff exists to describe. For the same reason paths are stored relative to the analysis root and slash-separated: `doppel analyze .` and a hook run rooted at an absolute cwd must produce comparable snapshots.

No wall-clock, no environment. A timestamp, hostname or absolute root inside a Snapshot would break byte-identical reproducibility the moment --format json exists. Those belong to the baseline file wrapper, which is never compared.

Index

Constants

View Source
const Schema = 2

Schema is the snapshot format version. Bump it whenever a field's meaning changes, so an older baseline is discarded rather than misread.

2 dropped every field no consumer read. A snapshot is diffed and rendered, never browsed, so a field nothing reads is pure weight in a file the Stop hook rewrites and re-parses on every turn.

Variables

This section is empty.

Functions

func Digest

func Digest(fp fingerprint.Fingerprint) string

Digest hashes a fingerprint into a short hex string: the exact, corpus-independent "this body changed" bit, and the most attributable signal a diff has.

All four fingerprint fields are already sorted and deduped by fingerprint.Build, so the hash is deterministic without any extra ordering work. The zero fingerprint (a declaration with no body) digests to the empty string rather than to a hash value, mirroring the rule that a zero fingerprint never matches anything: two body-less units must not be reported as having identical bodies.

Types

type Delta

type Delta struct {
	// Comparable is false when the two snapshots answer different questions.
	// Callers should say so and stop, not report a partial diff.
	Comparable bool   `json:"comparable"`
	Reason     string `json:"reason,omitempty"`

	FunctionsBefore int `json:"functionsBefore"`
	FunctionsAfter  int `json:"functionsAfter"`
	PairsBefore     int `json:"pairsBefore"`
	PairsAfter      int `json:"pairsAfter"`
	MergeBefore     int `json:"mergeWorthyBefore"`
	MergeAfter      int `json:"mergeWorthyAfter"`

	UnitsAdded    []Unit `json:"unitsAdded,omitempty"`   // sorted by key
	UnitsRemoved  []Unit `json:"unitsRemoved,omitempty"` // sorted by key
	BodiesChanged []Unit `json:"bodiesChanged,omitempty"`

	PairsAdded   []PairChange `json:"pairsAdded,omitempty"`   // sorted: attributable first, then score desc
	PairsRemoved []PairChange `json:"pairsRemoved,omitempty"` // sorted: attributable first, then score desc
	Drift        []Drift      `json:"drift,omitempty"`
}

Delta is what changed between two snapshots.

The fields are ordered by how much they can be trusted, and the renderer leads with the same order.

UnitsAdded, UnitsRemoved and BodiesChanged are the solid ground: they come from names and from the fingerprint digest, both of which depend only on a function's own AST. If a body digest moved, that function was edited, full stop.

PairsAdded and PairsRemoved are one tier down. Retrieval keeps a bounded number of neighbours per function, so a pair can enter or leave the candidate set without either side being touched. Each pair change therefore carries an Attributable bit saying whether at least one of its two sides is in the touched set; the renderer leads with the attributable ones, because those are the changes this session actually caused.

Everything else corpus-relative — role changes, caller and callee counts, overlap movement, tag totals — is deliberately absent. Those move when code nobody touched moves, and reporting them would be reporting the session for something it did not do.

func Diff

func Diff(base, head Snapshot) Delta

Diff compares a baseline against a later run.

Incomparability is a result, not an error: the hook has exactly one behaviour for every version of "these cannot be compared", and returning an error would tempt a caller into exiting non-zero over it. A mismatched schema, build, ontology or param set means the two runs measured different things, and a diff across them would be confidently wrong rather than merely unavailable.

func (Delta) AttributablePairs

func (d Delta) AttributablePairs() (added, removed []PairChange)

AttributablePairs returns the added and removed pairs this session's edits actually caused, which is what a report should lead with.

func (Delta) Empty

func (d Delta) Empty() bool

Empty reports whether the delta says nothing worth telling anyone. A hook that renders an empty delta should emit no output at all: a "nothing changed" line after every turn is worse than silence.

func (Delta) Touched

func (d Delta) Touched() map[string]bool

Touched reports whether a pair change can be attributed to an edit in this session, rather than to corpus drift. Under an unfiltered snapshot every added or removed pair should be attributable; one that is not indicates the snapshot was taken with a top-N or struct-min filter applied.

type Drift

type Drift struct {
	A                 string  `json:"a"`
	B                 string  `json:"b"`
	ScoreBefore       float64 `json:"scoreBefore"`
	ScoreAfter        float64 `json:"scoreAfter"`
	OverlapBefore     float64 `json:"overlapBefore"`
	OverlapAfter      float64 `json:"overlapAfter"`
	MergeWorthyBefore bool    `json:"mergeWorthyBefore"`
	MergeWorthyAfter  bool    `json:"mergeWorthyAfter"`
	Attributable      bool    `json:"attributable"` // at least one side's body changed
}

Drift is a pair present in both runs whose standing moved.

"Standing" is deliberately wider than "score". Merge-worthiness is gated on overlap, not on shape score, so a pair can cross into or out of merge-worthy with its score untouched. Admitting only score movement would make exactly the most consequential drift — the kind that changes what you should do about a pair — the kind that never gets recorded.

func (Drift) CrossedGate

func (d Drift) CrossedGate() bool

CrossedGate reports whether this pair changed merge-worthiness. It is the only drift that changes a decision, so it is what a short report leads with.

func (Drift) ScoreMoved

func (d Drift) ScoreMoved() float64

ScoreMoved is the absolute shape-score movement.

type Pair

type Pair struct {
	A           string  `json:"a"`
	B           string  `json:"b"`
	Score       float64 `json:"score"`
	Overlap     float64 `json:"overlap"`     // corpus-relative
	MergeWorthy bool    `json:"mergeWorthy"` // half corpus-relative
}

Pair is one reported near-duplicate. A and B are Unit keys, ordered A < B so a pair has exactly one spelling and can be matched across runs.

Score is corpus-independent: fingerprint.Similarity reads two fingerprints and nothing else. Overlap is corpus-weighted through the information content of this run's tag counts, and MergeWorthy is half so — the signal count is corpus-independent but the 0.4 overlap gate is not.

Earlier schemas also carried the four fingerprint.Breakdown components and the evidence Reasons strings. Neither was ever read back: the text report renders both straight off analyzer.SimilarPair, never through a snapshot. The Reasons in particular were a quarter of a baseline's bytes — free-text English restating counts that move with corpus churn.

func (Pair) Key

func (p Pair) Key() string

Key is the identity of a pair across runs.

type PairChange

type PairChange struct {
	Pair
	// Attributable is true when at least one side was added or had its body
	// changed. When false the pair moved because retrieval re-ranked around it,
	// which is corpus churn rather than a consequence of the edit.
	Attributable bool `json:"attributable"`
}

PairChange is a pair that entered or left the candidate set, carrying whether the change can be traced to a function this session actually edited.

type Params

type Params struct {
	Threshold  float64 `json:"threshold"`
	Top        int     `json:"top"`
	MinNodes   int     `json:"minNodes"`
	StructMin  float64 `json:"structMin"`
	ChannelK   int     `json:"channelK"`
	MaxPerFunc int     `json:"maxPerFunc"`
	TestsMode  string  `json:"testsMode"`
}

Params records the knobs a run used. Diff compares them because every doppel score is corpus-relative: a baseline taken at a different threshold or min-nodes is not an earlier answer to the same question, it is an answer to a different one.

type RoleCount

type RoleCount struct {
	Role  string `json:"role"`
	Count int    `json:"count"`
}

RoleCount is one structural role and how many units were classified into it.

type Snapshot

type Snapshot struct {
	Schema    int         `json:"schema"`
	Doppel    string      `json:"doppel"`   // doppel build version
	Ontology  string      `json:"ontology"` // ontology.Version the run reasoned with
	Params    Params      `json:"params"`
	Functions int         `json:"functions"`
	Concepts  []TagCount  `json:"concepts"` // sorted by tag
	Roles     []RoleCount `json:"roles"`    // sorted by role
	Units     []Unit      `json:"units"`    // sorted by key
	Pairs     []Pair      `json:"pairs"`    // sorted by score desc, then a, then b
}

Snapshot is one full analysis run.

A snapshot intended for diffing is built from the retrieved candidate set before the report's presentation cutoffs — no top-N truncation, no per-function diversity cap, no struct-min filter. Those cutoffs drop a pair for reasons that have nothing to do with any edit, so applying them first would manufacture differences; they belong at render time.

Removing them narrows the noise but does not eliminate it, and the schema is honest about which. Retrieval keeps each function's top-K neighbours per channel, weighted by corpus rarity, so pair membership is corpus-relative: adding code elsewhere can push a pair out of a function's channel budget without either body changing. That is why Diff separates a pair change it can attribute to a fingerprint that actually moved from one it cannot — see Delta.

func Build

func Build(units []parser.CodeUnit, docs []concepter.ConceptDoc, pairs []analyzer.SimilarPair,
	tagCounts map[ontology.TermID]int, root, version string, p Params) Snapshot

Build assembles a Snapshot from one pipeline run.

docs[i] must describe units[i] and pairs must carry AIdx/BIdx into units: that positional alignment is the pipeline's existing contract, and resolving it by name instead has already caused one silent-miss bug in this codebase. Build converts to names once, here, at the boundary where positions stop being meaningful.

func (Snapshot) MergeWorthy

func (s Snapshot) MergeWorthy() int

MergeWorthy counts the pairs the run judged worth merging.

func (Snapshot) UnitByKey

func (s Snapshot) UnitByKey() map[string]Unit

UnitByKey indexes the snapshot's units for lookup during a diff.

type TagCount

type TagCount struct {
	Tag   string `json:"tag"`
	Count int    `json:"count"`
}

TagCount is one concept tag and how many units carry it.

type Unit

type Unit struct {
	Key      string   `json:"key"` // stable cross-run identity; see unitKeys
	Package  string   `json:"package"`
	Name     string   `json:"name"`
	File     string   `json:"file"` // relative to root, slash-separated
	Line     int      `json:"line"` // display only, never diffed
	Patterns []string `json:"patterns,omitempty"`
	Digest   string   `json:"digest"` // fingerprint hash: the exact "body changed" bit
	Role     string   `json:"role"`   // corpus-relative; documented output only
}

Unit is one function or method as this run saw it.

Only what a consumer reads is kept. Key and Digest are corpus-independent — they depend on this function's own AST alone — and together they are the whole of what Diff may claim: Key recognises a function across runs, Digest is the exact "this body changed" bit. Package and Patterns feed the concept inventory, File and Line locate a finding for a human, and Line is display only: inserting anything above a function shifts it.

Role is corpus-relative and no internal consumer reads it. It survives because `analyze --format json` documents it, not because anything here needs it — see the --format row in README.md before removing it.

Earlier schemas also carried Qualified, Exported, Receiver, Nodes, Callers and Callees. Nothing ever read them.

Jump to

Keyboard shortcuts

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